blob: 225b335ec659660c2d23dd514e094cd0d9f2faca [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_frame_observer.h"
#include <limits>
#include "base/metrics/histogram_macros.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/node_computed_style.h"
#include "third_party/blink/renderer/core/frame/frame.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/geometry/dom_rect_read_only.h"
#include "third_party/blink/renderer/core/html/html_frame_owner_element.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/loader/frame_load_request.h"
#include "third_party/blink/renderer/core/loader/frame_loader.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
#include "third_party/blink/renderer/platform/geometry/length.h"
#include "third_party/blink/renderer/platform/heap/visitor.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_request.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/assertions.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
namespace blink {
namespace {
// Determine if the |bounding_client_rect| for a frame indicates that the frame
// is probably hidden according to some experimental heuristics. Since hidden
// frames are often used for analytics or communication, and lazily loading them
// could break their functionality, so these heuristics are used to recognize
// likely hidden frames and immediately load them so that they can function
// properly.
bool IsFrameProbablyHidden(const DOMRectReadOnly& bounding_client_rect,
const Element& element) {
// Tiny frames that are 4x4 or smaller are likely not intended to be seen by
// the user. Note that this condition includes frames marked as
// "display:none", since those frames would have dimensions of 0x0.
if (bounding_client_rect.width() < 4.1 || bounding_client_rect.height() < 4.1)
return true;
// Frames that are positioned completely off the page above or to the left are
// likely never intended to be visible to the user.
if (bounding_client_rect.right() < 0.0 || bounding_client_rect.bottom() < 0.0)
return true;
const ComputedStyle* style = element.GetComputedStyle();
if (style) {
switch (style->Visibility()) {
case EVisibility::kHidden:
case EVisibility::kCollapse:
return true;
case EVisibility::kVisible:
break;
}
}
return false;
}
int GetLazyFrameLoadingViewportDistanceThresholdPx(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->GetLazyFrameLoadingDistanceThresholdPxUnknown();
case WebEffectiveConnectionType::kTypeOffline:
return settings->GetLazyFrameLoadingDistanceThresholdPxOffline();
case WebEffectiveConnectionType::kTypeSlow2G:
return settings->GetLazyFrameLoadingDistanceThresholdPxSlow2G();
case WebEffectiveConnectionType::kType2G:
return settings->GetLazyFrameLoadingDistanceThresholdPx2G();
case WebEffectiveConnectionType::kType3G:
return settings->GetLazyFrameLoadingDistanceThresholdPx3G();
case WebEffectiveConnectionType::kType4G:
return settings->GetLazyFrameLoadingDistanceThresholdPx4G();
}
NOTREACHED();
return 0;
}
} // namespace
struct LazyLoadFrameObserver::LazyLoadRequestInfo {
LazyLoadRequestInfo(const ResourceRequest& resource_request,
WebFrameLoadType frame_load_type)
: resource_request(resource_request), frame_load_type(frame_load_type) {}
const ResourceRequest resource_request;
const WebFrameLoadType frame_load_type;
};
LazyLoadFrameObserver::LazyLoadFrameObserver(HTMLFrameOwnerElement& element)
: element_(&element) {}
LazyLoadFrameObserver::~LazyLoadFrameObserver() = default;
void LazyLoadFrameObserver::DeferLoadUntilNearViewport(
const ResourceRequest& resource_request,
WebFrameLoadType frame_load_type) {
DCHECK(!lazy_load_intersection_observer_);
DCHECK(!lazy_load_request_info_);
lazy_load_request_info_ =
std::make_unique<LazyLoadRequestInfo>(resource_request, frame_load_type);
was_recorded_as_deferred_ = false;
lazy_load_intersection_observer_ = IntersectionObserver::Create(
{Length(GetLazyFrameLoadingViewportDistanceThresholdPx(
element_->GetDocument()),
kFixed)},
{std::numeric_limits<float>::min()}, &element_->GetDocument(),
WTF::BindRepeating(&LazyLoadFrameObserver::LoadIfHiddenOrNearViewport,
WrapWeakPersistent(this)));
lazy_load_intersection_observer_->observe(element_);
}
void LazyLoadFrameObserver::CancelPendingLazyLoad() {
lazy_load_request_info_.reset();
if (!lazy_load_intersection_observer_)
return;
lazy_load_intersection_observer_->disconnect();
lazy_load_intersection_observer_.Clear();
}
void LazyLoadFrameObserver::LoadIfHiddenOrNearViewport(
const HeapVector<Member<IntersectionObserverEntry>>& entries) {
DCHECK(!entries.IsEmpty());
DCHECK_EQ(element_, entries.back()->target());
if (entries.back()->isIntersecting()) {
RecordInitialDeferralAction(
FrameInitialDeferralAction::kLoadedNearOrInViewport);
} else if (IsFrameProbablyHidden(*entries.back()->boundingClientRect(),
*element_)) {
RecordInitialDeferralAction(FrameInitialDeferralAction::kLoadedHidden);
} else {
RecordInitialDeferralAction(FrameInitialDeferralAction::kDeferred);
return;
}
LoadImmediately();
}
void LazyLoadFrameObserver::LoadImmediately() {
DCHECK(IsLazyLoadPending());
DCHECK(lazy_load_request_info_);
if (was_recorded_as_deferred_) {
DCHECK(element_->GetDocument().GetFrame());
DCHECK(element_->GetDocument().GetFrame()->Client());
UMA_HISTOGRAM_ENUMERATION(
"Blink.LazyLoad.CrossOriginFrames.LoadStartedAfterBeingDeferred",
element_->GetDocument()
.GetFrame()
->Client()
->GetEffectiveConnectionType());
}
std::unique_ptr<LazyLoadRequestInfo> scoped_request_info =
std::move(lazy_load_request_info_);
// The content frame of the element should not have changed, since any
// pending lazy load should have been already been cancelled in
// DisconnectContentFrame() if the content frame changes.
DCHECK(element_->ContentFrame());
// Note that calling FrameLoader::StartNavigation() causes the
// |lazy_load_intersection_observer_| to be disconnected.
ToLocalFrame(element_->ContentFrame())
->Loader()
.StartNavigation(FrameLoadRequest(&element_->GetDocument(),
scoped_request_info->resource_request),
scoped_request_info->frame_load_type);
DCHECK(!IsLazyLoadPending());
}
void LazyLoadFrameObserver::StartTrackingVisibilityMetrics() {
DCHECK(time_when_first_visible_.is_null());
DCHECK(!visibility_metrics_observer_);
visibility_metrics_observer_ = IntersectionObserver::Create(
{}, {std::numeric_limits<float>::min()}, &element_->GetDocument(),
WTF::BindRepeating(
&LazyLoadFrameObserver::RecordMetricsOnVisibilityChanged,
WrapWeakPersistent(this)));
visibility_metrics_observer_->observe(element_);
}
void LazyLoadFrameObserver::RecordMetricsOnVisibilityChanged(
const HeapVector<Member<IntersectionObserverEntry>>& entries) {
DCHECK(!entries.IsEmpty());
DCHECK_EQ(element_, entries.back()->target());
if (IsFrameProbablyHidden(*entries.back()->boundingClientRect(), *element_)) {
visibility_metrics_observer_->disconnect();
visibility_metrics_observer_.Clear();
return;
}
if (!has_above_the_fold_been_set_) {
is_initially_above_the_fold_ = entries.back()->isIntersecting();
has_above_the_fold_been_set_ = true;
}
if (!entries.back()->isIntersecting())
return;
DCHECK(time_when_first_visible_.is_null());
time_when_first_visible_ = CurrentTimeTicks();
RecordVisibilityMetricsIfLoadedAndVisible();
visibility_metrics_observer_->disconnect();
visibility_metrics_observer_.Clear();
// The below metrics require getting the effective connection type from the
// parent frame, so return early here if there's no parent frame to get the
// effective connection type from.
if (!element_->GetDocument().GetFrame())
return;
DCHECK(element_->GetDocument().GetFrame()->Client());
// On slow networks, iframes might not finish loading by the time the user
// leaves the page, so the visible load time metrics samples won't represent
// the slowest frames. To remedy this, record how often below the fold
// lazyload-eligible frames become visible before they've finished loading.
// This isn't recorded for above the fold frames since basically every above
// the fold frame would be visible before they finish loading.
if (time_when_first_load_finished_.is_null() &&
!is_initially_above_the_fold_) {
// Note: If the WebEffectiveConnectionType enum ever gets out of sync with
// net::EffectiveConnectionType, then this will have to be updated to record
// the sample in terms of net::EffectiveConnectionType instead of
// WebEffectiveConnectionType.
UMA_HISTOGRAM_ENUMERATION(
"Blink.VisibleBeforeLoaded.LazyLoadEligibleFrames.BelowTheFold",
element_->GetDocument()
.GetFrame()
->Client()
->GetEffectiveConnectionType());
}
if (was_recorded_as_deferred_) {
UMA_HISTOGRAM_ENUMERATION(
"Blink.LazyLoad.CrossOriginFrames.VisibleAfterBeingDeferred",
element_->GetDocument()
.GetFrame()
->Client()
->GetEffectiveConnectionType());
}
}
void LazyLoadFrameObserver::RecordMetricsOnLoadFinished() {
if (!time_when_first_load_finished_.is_null())
return;
time_when_first_load_finished_ = CurrentTimeTicks();
RecordVisibilityMetricsIfLoadedAndVisible();
}
void LazyLoadFrameObserver::RecordVisibilityMetricsIfLoadedAndVisible() {
if (time_when_first_load_finished_.is_null() ||
time_when_first_visible_.is_null() ||
!element_->GetDocument().GetFrame()) {
return;
}
DCHECK(has_above_the_fold_been_set_);
TimeDelta visible_load_delay =
time_when_first_load_finished_ - time_when_first_visible_;
if (visible_load_delay < TimeDelta())
visible_load_delay = TimeDelta();
switch (element_->GetDocument()
.GetFrame()
->Client()
->GetEffectiveConnectionType()) {
case WebEffectiveConnectionType::kTypeSlow2G:
if (is_initially_above_the_fold_) {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadEligibleFrames.AboveTheFold.Slow2G",
visible_load_delay);
} else {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadEligibleFrames.BelowTheFold.Slow2G",
visible_load_delay);
}
break;
case WebEffectiveConnectionType::kType2G:
if (is_initially_above_the_fold_) {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadEligibleFrames.AboveTheFold.2G",
visible_load_delay);
} else {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadEligibleFrames.BelowTheFold.2G",
visible_load_delay);
}
break;
case WebEffectiveConnectionType::kType3G:
if (is_initially_above_the_fold_) {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadEligibleFrames.AboveTheFold.3G",
visible_load_delay);
} else {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadEligibleFrames.BelowTheFold.3G",
visible_load_delay);
}
break;
case WebEffectiveConnectionType::kType4G:
if (is_initially_above_the_fold_) {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadEligibleFrames.AboveTheFold.4G",
visible_load_delay);
} else {
UMA_HISTOGRAM_MEDIUM_TIMES(
"Blink.VisibleLoadTime.LazyLoadEligibleFrames.BelowTheFold.4G",
visible_load_delay);
}
break;
case WebEffectiveConnectionType::kTypeUnknown:
case WebEffectiveConnectionType::kTypeOffline:
// No VisibleLoadTime histograms are recorded for these effective
// connection types.
break;
}
}
void LazyLoadFrameObserver::RecordInitialDeferralAction(
FrameInitialDeferralAction action) {
if (was_recorded_as_deferred_)
return;
DCHECK(element_->GetDocument().GetFrame());
DCHECK(element_->GetDocument().GetFrame()->Client());
switch (element_->GetDocument()
.GetFrame()
->Client()
->GetEffectiveConnectionType()) {
case WebEffectiveConnectionType::kTypeUnknown:
UMA_HISTOGRAM_ENUMERATION(
"Blink.LazyLoad.CrossOriginFrames.InitialDeferralAction.Unknown",
action);
break;
case WebEffectiveConnectionType::kTypeOffline:
UMA_HISTOGRAM_ENUMERATION(
"Blink.LazyLoad.CrossOriginFrames.InitialDeferralAction.Offline",
action);
break;
case WebEffectiveConnectionType::kTypeSlow2G:
UMA_HISTOGRAM_ENUMERATION(
"Blink.LazyLoad.CrossOriginFrames.InitialDeferralAction.Slow2G",
action);
break;
case WebEffectiveConnectionType::kType2G:
UMA_HISTOGRAM_ENUMERATION(
"Blink.LazyLoad.CrossOriginFrames.InitialDeferralAction.2G", action);
break;
case WebEffectiveConnectionType::kType3G:
UMA_HISTOGRAM_ENUMERATION(
"Blink.LazyLoad.CrossOriginFrames.InitialDeferralAction.3G", action);
break;
case WebEffectiveConnectionType::kType4G:
UMA_HISTOGRAM_ENUMERATION(
"Blink.LazyLoad.CrossOriginFrames.InitialDeferralAction.4G", action);
break;
}
if (action == FrameInitialDeferralAction::kDeferred)
was_recorded_as_deferred_ = true;
}
void LazyLoadFrameObserver::Trace(blink::Visitor* visitor) {
visitor->Trace(element_);
visitor->Trace(lazy_load_intersection_observer_);
visitor->Trace(visibility_metrics_observer_);
}
} // namespace blink