blob: 32c4a33e9d4e1fab0b858e4ff8880bf25e3e1fe3 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/html/lazy_load_image_observer.h"
#include <limits>
#include "base/metrics/histogram_macros.h"
#include "build/build_config.h"
#include "third_party/blink/public/platform/web_effective_connection_type.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html_element_type_helpers.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/intersection_observer/intersection_observer.h"
#include "third_party/blink/renderer/core/intersection_observer/intersection_observer_entry.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/network/network_state_notifier.h"
namespace blink {
namespace {
int GetLazyImageLoadingViewportDistanceThresholdPx(const Document& document) {
const Settings* settings = document.GetSettings();
if (!settings)
return 0;
switch (GetNetworkStateNotifier().EffectiveType()) {
case WebEffectiveConnectionType::kTypeUnknown:
return settings->GetLazyImageLoadingDistanceThresholdPxUnknown();
case WebEffectiveConnectionType::kTypeOffline:
return settings->GetLazyImageLoadingDistanceThresholdPxOffline();
case WebEffectiveConnectionType::kTypeSlow2G:
return settings->GetLazyImageLoadingDistanceThresholdPxSlow2G();
case WebEffectiveConnectionType::kType2G:
return settings->GetLazyImageLoadingDistanceThresholdPx2G();
case WebEffectiveConnectionType::kType3G:
return settings->GetLazyImageLoadingDistanceThresholdPx3G();
case WebEffectiveConnectionType::kType4G:
return settings->GetLazyImageLoadingDistanceThresholdPx4G();
}
NOTREACHED();
return 0;
}
int GetFirstKFullyLoadCount(const Document& document) {
const Settings* settings = document.GetSettings();
if (!settings)
return 0;
switch (GetNetworkStateNotifier().EffectiveType()) {
case WebEffectiveConnectionType::kTypeOffline:
return 0;
case WebEffectiveConnectionType::kTypeUnknown:
return settings->GetLazyImageFirstKFullyLoadUnknown();
case WebEffectiveConnectionType::kTypeSlow2G:
return settings->GetLazyImageFirstKFullyLoadSlow2G();
case WebEffectiveConnectionType::kType2G:
return settings->GetLazyImageFirstKFullyLoad2G();
case WebEffectiveConnectionType::kType3G:
return settings->GetLazyImageFirstKFullyLoad3G();
case WebEffectiveConnectionType::kType4G:
return settings->GetLazyImageFirstKFullyLoad4G();
}
NOTREACHED();
return 0;
}
// Returns if the element or its ancestors are invisible, due to their style or
// attribute or due to themselves not connected to the main document tree.
bool IsElementInInvisibleSubTree(const Element& element) {
if (!element.isConnected())
return true;
for (Node& ancestor : FlatTreeTraversal::InclusiveAncestorsOf(element)) {
auto* ancestor_element = DynamicTo<Element>(ancestor);
if (!ancestor_element)
continue;
const ComputedStyle* style = ancestor_element->EnsureComputedStyle();
if (style && (style->Visibility() != EVisibility::kVisible ||
style->Display() == EDisplay::kNone)) {
return true;
}
}
return false;
}
void RecordVisibleLoadTimeForImage(
const LazyLoadImageObserver::VisibleLoadTimeMetrics&
visible_load_time_metrics) {
DCHECK(visible_load_time_metrics.has_initial_intersection_been_set);
DCHECK(!visible_load_time_metrics.time_when_first_visible.is_null());
DCHECK(!visible_load_time_metrics.time_when_first_load_finished.is_null());
base::TimeDelta visible_load_delay =
visible_load_time_metrics.time_when_first_load_finished -
visible_load_time_metrics.time_when_first_visible;
if (visible_load_delay < base::TimeDelta())
visible_load_delay = base::TimeDelta();
switch (GetNetworkStateNotifier().EffectiveType()) {
case WebEffectiveConnectionType::kTypeSlow2G:
if (visible_load_time_metrics.is_initially_intersecting) {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadImages.AboveTheFold.Slow2G",
visible_load_delay);
} else {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadImages.BelowTheFold.Slow2G",
visible_load_delay);
}
break;
case WebEffectiveConnectionType::kType2G:
if (visible_load_time_metrics.is_initially_intersecting) {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadImages.AboveTheFold.2G",
visible_load_delay);
} else {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadImages.BelowTheFold.2G",
visible_load_delay);
}
break;
case WebEffectiveConnectionType::kType3G:
if (visible_load_time_metrics.is_initially_intersecting) {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadImages.AboveTheFold.3G",
visible_load_delay);
} else {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadImages.BelowTheFold.3G",
visible_load_delay);
}
break;
case WebEffectiveConnectionType::kType4G:
if (visible_load_time_metrics.is_initially_intersecting) {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadImages.AboveTheFold.4G",
visible_load_delay);
} else {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadImages.BelowTheFold.4G",
visible_load_delay);
}
break;
case WebEffectiveConnectionType::kTypeUnknown:
case WebEffectiveConnectionType::kTypeOffline:
// No VisibleLoadTime histograms are recorded for these effective
// connection types.
break;
}
}
} // namespace
LazyLoadImageObserver::LazyLoadImageObserver(const Document& document)
: count_remaining_images_fully_loaded_(GetFirstKFullyLoadCount(document)) {}
void LazyLoadImageObserver::StartMonitoringNearViewport(
Document* root_document,
Element* element,
DeferralMessage deferral_message) {
DCHECK(RuntimeEnabledFeatures::LazyImageLoadingEnabled());
if (!lazy_load_intersection_observer_) {
lazy_load_intersection_observer_ = IntersectionObserver::Create(
{Length::Fixed(
GetLazyImageLoadingViewportDistanceThresholdPx(*root_document))},
{std::numeric_limits<float>::min()}, root_document,
WTF::BindRepeating(&LazyLoadImageObserver::LoadIfNearViewport,
WrapWeakPersistent(this)));
}
lazy_load_intersection_observer_->observe(element);
if (deferral_message == DeferralMessage::kLoadEventsDeferred &&
!is_load_event_deferred_intervention_shown_) {
is_load_event_deferred_intervention_shown_ = true;
root_document->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kIntervention,
mojom::ConsoleMessageLevel::kInfo,
"Images loaded lazily and replaced with placeholders. Load events are "
"deferred. See https://crbug.com/954323"));
}
if (deferral_message == DeferralMessage::kMissingDimensionForLazy &&
!is_missing_dimension_intervention_shown_) {
is_missing_dimension_intervention_shown_ = true;
root_document->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kIntervention,
mojom::ConsoleMessageLevel::kInfo,
"An <img> element was lazyloaded with loading=lazy, but had no "
"dimensions specified. Specifying dimensions improves performance. See "
"https://crbug.com/954323"));
UseCounter::Count(root_document,
WebFeature::kLazyLoadImageMissingDimensionsForLazy);
}
}
void LazyLoadImageObserver::StopMonitoring(Element* element) {
lazy_load_intersection_observer_->unobserve(element);
}
void LazyLoadImageObserver::LoadIfNearViewport(
const HeapVector<Member<IntersectionObserverEntry>>& entries) {
DCHECK(!entries.IsEmpty());
for (auto entry : entries) {
Element* element = entry->target();
auto* image_element = DynamicTo<HTMLImageElement>(element);
// If the loading_attr is 'lazy' explicitly, we'd better to wait for
// intersection.
if (!entry->isIntersecting() && image_element &&
!EqualIgnoringASCIICase(image_element->FastGetAttribute(html_names::kLoadingAttr), "lazy")) {
// Fully load the invisible image elements. The elements can be invisible
// by style such as display:none, visibility: hidden, or hidden via
// attribute, etc. Style might also not be calculated if the ancestors
// were invisible.
const ComputedStyle* style = entry->target()->GetComputedStyle();
if (!style || style->Visibility() != EVisibility::kVisible ||
style->Display() == EDisplay::kNone) {
// Check that style was null because it was not computed since the
// element was in an invisible subtree.
DCHECK(style || IsElementInInvisibleSubTree(*element));
image_element->LoadDeferredImage();
lazy_load_intersection_observer_->unobserve(element);
}
}
if (!entry->isIntersecting())
continue;
if (image_element)
image_element->LoadDeferredImage();
// Load the background image if the element has one deferred.
if (const ComputedStyle* style = element->GetComputedStyle())
style->LoadDeferredImages(element->GetDocument());
lazy_load_intersection_observer_->unobserve(element);
}
}
void LazyLoadImageObserver::StartMonitoringVisibility(
Document* root_document,
HTMLImageElement* image_element) {
DCHECK(RuntimeEnabledFeatures::LazyImageVisibleLoadTimeMetricsEnabled());
VisibleLoadTimeMetrics& visible_load_time_metrics =
image_element->EnsureVisibleLoadTimeMetrics();
if (!visible_load_time_metrics.time_when_first_visible.is_null()) {
// The time when the image first became visible has already been measured,
// so there's no need to monitor the visibility of the image any more.
return;
}
if (!visibility_metrics_observer_) {
visibility_metrics_observer_ = IntersectionObserver::Create(
{}, {std::numeric_limits<float>::min()}, root_document,
WTF::BindRepeating(&LazyLoadImageObserver::OnVisibilityChanged,
WrapWeakPersistent(this)));
}
visibility_metrics_observer_->observe(image_element);
}
void LazyLoadImageObserver::OnLoadFinished(HTMLImageElement* image_element) {
DCHECK(RuntimeEnabledFeatures::LazyImageVisibleLoadTimeMetricsEnabled());
VisibleLoadTimeMetrics& visible_load_time_metrics =
image_element->EnsureVisibleLoadTimeMetrics();
if (!visible_load_time_metrics.time_when_first_load_finished.is_null())
return;
visible_load_time_metrics.time_when_first_load_finished =
base::TimeTicks::Now();
if (visible_load_time_metrics.time_when_first_visible.is_null())
return;
RecordVisibleLoadTimeForImage(visible_load_time_metrics);
}
void LazyLoadImageObserver::OnVisibilityChanged(
const HeapVector<Member<IntersectionObserverEntry>>& entries) {
DCHECK(!entries.IsEmpty());
for (auto entry : entries) {
auto* image_element = DynamicTo<HTMLImageElement>(entry->target());
if (!image_element)
continue;
VisibleLoadTimeMetrics& visible_load_time_metrics =
image_element->EnsureVisibleLoadTimeMetrics();
// The image's visiblity shouldn't still be monitored if the time when the
// image first became visible has already been measured.
DCHECK(visible_load_time_metrics.time_when_first_visible.is_null());
if (!visible_load_time_metrics.has_initial_intersection_been_set) {
visible_load_time_metrics.has_initial_intersection_been_set = true;
visible_load_time_metrics.is_initially_intersecting =
entry->isIntersecting();
}
if (!entry->isIntersecting())
continue;
visible_load_time_metrics.time_when_first_visible = base::TimeTicks::Now();
if (visible_load_time_metrics.time_when_first_load_finished.is_null()) {
// Note: If the WebEffectiveConnectionType enum ever gets out of sync
// with net::EffectiveConnectionType, then both the AboveTheFold and
// BelowTheFold histograms here will have to be updated to record the
// sample in terms of net::EffectiveConnectionType instead of
// WebEffectiveConnectionType.
if (visible_load_time_metrics.is_initially_intersecting) {
UMA_HISTOGRAM_ENUMERATION(
"Blink.VisibleBeforeLoaded.LazyLoadImages.AboveTheFold",
GetNetworkStateNotifier().EffectiveType());
} else {
UMA_HISTOGRAM_ENUMERATION(
"Blink.VisibleBeforeLoaded.LazyLoadImages.BelowTheFold",
GetNetworkStateNotifier().EffectiveType());
}
} else {
RecordVisibleLoadTimeForImage(visible_load_time_metrics);
}
visibility_metrics_observer_->unobserve(image_element);
}
}
bool LazyLoadImageObserver::IsFullyLoadableFirstKImageAndDecrementCount() {
if (count_remaining_images_fully_loaded_ > 0) {
count_remaining_images_fully_loaded_--;
return true;
}
return false;
}
void LazyLoadImageObserver::Trace(Visitor* visitor) {
visitor->Trace(lazy_load_intersection_observer_);
visitor->Trace(visibility_metrics_observer_);
}
} // namespace blink