blob: 2f10b829b717263b2ffd20b564a9fa26e8dddd4a [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/node_computed_style.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/frame/settings.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/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"
namespace blink {
namespace {
int GetLazyImageLoadingViewportDistanceThresholdPx(const Document& document) {
const Settings* settings = document.GetSettings();
if (!settings)
return 0;
DCHECK(document.GetFrame() && document.GetFrame()->Client());
switch (document.GetFrame()->Client()->GetEffectiveConnectionType()) {
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;
}
Document* GetRootDocumentOrNull(Element* element) {
if (LocalFrame* frame = element->GetDocument().GetFrame())
return frame->LocalFrameRoot().GetDocument();
return nullptr;
}
} // namespace
void LazyLoadImageObserver::StartMonitoring(Element* element) {
if (Document* document = GetRootDocumentOrNull(element)) {
document->EnsureLazyLoadImageObserver().StartMonitoringNearViewport(
document, element);
}
}
void LazyLoadImageObserver::StopMonitoring(Element* element) {
if (Document* document = GetRootDocumentOrNull(element)) {
document->EnsureLazyLoadImageObserver()
.lazy_load_intersection_observer_->unobserve(element);
}
}
void LazyLoadImageObserver::StartTrackingVisibilityMetrics(
HTMLImageElement* image_element) {
if (!RuntimeEnabledFeatures::LazyImageVisibleLoadTimeMetricsEnabled())
return;
if (Document* document = GetRootDocumentOrNull(image_element)) {
document->EnsureLazyLoadImageObserver().StartMonitoringVisibility(
document, image_element);
}
}
void LazyLoadImageObserver::RecordMetricsOnLoadFinished(
HTMLImageElement* image_element) {
if (!RuntimeEnabledFeatures::LazyImageVisibleLoadTimeMetricsEnabled())
return;
if (Document* document = GetRootDocumentOrNull(image_element)) {
document->EnsureLazyLoadImageObserver().OnLoadFinished(image_element);
}
}
LazyLoadImageObserver::LazyLoadImageObserver() = default;
void LazyLoadImageObserver::StartMonitoringNearViewport(Document* root_document,
Element* element) {
DCHECK(RuntimeEnabledFeatures::LazyImageLoadingEnabled());
if (!lazy_load_intersection_observer_) {
root_document->AddConsoleMessage(ConsoleMessage::Create(
kInterventionMessageSource, kInfoMessageLevel,
"Images loaded lazily and replaced with placeholders. Load events are "
"deferred. See https://crbug.com/846170"));
lazy_load_intersection_observer_ = IntersectionObserver::Create(
{Length(GetLazyImageLoadingViewportDistanceThresholdPx(*root_document),
kFixed)},
{std::numeric_limits<float>::min()}, root_document,
WTF::BindRepeating(&LazyLoadImageObserver::LoadIfNearViewport,
WrapWeakPersistent(this)));
}
lazy_load_intersection_observer_->observe(element);
}
void LazyLoadImageObserver::LoadIfNearViewport(
const HeapVector<Member<IntersectionObserverEntry>>& entries) {
DCHECK(!entries.IsEmpty());
for (auto entry : entries) {
if (!entry->isIntersecting())
continue;
Element* element = entry->target();
if (auto* image_element = ToHTMLImageElementOrNull(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.has_initial_intersection_been_set) {
// The element has already been monitored.
return;
}
if (!visibility_metrics_observer_) {
visibility_metrics_observer_ = IntersectionObserver::Create(
{}, {std::numeric_limits<float>::min()}, root_document,
WTF::BindRepeating(&LazyLoadImageObserver::OnVisibilityChanged,
WrapWeakPersistent(this)));
}
visible_load_time_metrics.record_visibility_metrics = true;
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.record_visibility_metrics)
return;
visible_load_time_metrics.record_visibility_metrics = false;
visibility_metrics_observer_->unobserve(image_element);
TimeDelta visible_load_delay;
if (!visible_load_time_metrics.time_when_first_visible.is_null()) {
visible_load_delay =
CurrentTimeTicks() - visible_load_time_metrics.time_when_first_visible;
}
switch (image_element->GetDocument()
.GetFrame()
->Client()
->GetEffectiveConnectionType()) {
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;
}
}
void LazyLoadImageObserver::OnVisibilityChanged(
const HeapVector<Member<IntersectionObserverEntry>>& entries) {
DCHECK(!entries.IsEmpty());
for (auto entry : entries) {
if (auto* image_element = ToHTMLImageElementOrNull(entry->target())) {
VisibleLoadTimeMetrics& visible_load_time_metrics =
image_element->EnsureVisibleLoadTimeMetrics();
if (!visible_load_time_metrics.has_initial_intersection_been_set) {
visible_load_time_metrics.is_initially_intersecting =
entry->isIntersecting();
visible_load_time_metrics.has_initial_intersection_been_set = true;
}
if (entry->isIntersecting()) {
DCHECK(visible_load_time_metrics.time_when_first_visible.is_null());
visible_load_time_metrics.time_when_first_visible = CurrentTimeTicks();
if (visible_load_time_metrics.record_visibility_metrics &&
image_element->GetDocument().GetFrame()) {
// Since the visibility metrics are recorded when the image finishes
// loading, this means that the image became visible before it
// finished loading.
// 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",
image_element->GetDocument()
.GetFrame()
->Client()
->GetEffectiveConnectionType());
} else {
UMA_HISTOGRAM_ENUMERATION(
"Blink.VisibleBeforeLoaded.LazyLoadImages.BelowTheFold",
image_element->GetDocument()
.GetFrame()
->Client()
->GetEffectiveConnectionType());
}
}
visibility_metrics_observer_->unobserve(image_element);
}
}
}
}
void LazyLoadImageObserver::Trace(Visitor* visitor) {
visitor->Trace(lazy_load_intersection_observer_);
visitor->Trace(visibility_metrics_observer_);
}
} // namespace blink