| // 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/public/web/web_local_frame_client.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/network/network_state_notifier.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 PhysicalRect& 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; |
| |
| switch (GetNetworkStateNotifier().EffectiveType()) { |
| 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 ResourceRequestHead& passed_resource_request, |
| WebFrameLoadType frame_load_type) |
| : resource_request(passed_resource_request), |
| frame_load_type(frame_load_type) {} |
| |
| ResourceRequestHead resource_request; |
| const WebFrameLoadType frame_load_type; |
| }; |
| |
| LazyLoadFrameObserver::LazyLoadFrameObserver(HTMLFrameOwnerElement& element) |
| : element_(&element) {} |
| |
| LazyLoadFrameObserver::~LazyLoadFrameObserver() = default; |
| |
| void LazyLoadFrameObserver::DeferLoadUntilNearViewport( |
| const ResourceRequestHead& 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::Fixed(GetLazyFrameLoadingViewportDistanceThresholdPx( |
| element_->GetDocument()))}, |
| {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()->GetGeometry().TargetRect(), |
| *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", |
| GetNetworkStateNotifier().EffectiveType()); |
| element_->GetDocument().GetFrame()->Client()->DidObserveLazyLoadBehavior( |
| WebLocalFrameClient::LazyLoadBehavior::kLazyLoadedFrame); |
| } |
| |
| 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. |
| FrameLoadRequest request(&element_->GetDocument(), |
| scoped_request_info->resource_request); |
| To<LocalFrame>(element_->ContentFrame()) |
| ->Loader() |
| .StartNavigation(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()->GetGeometry().TargetRect(), |
| *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_ = base::TimeTicks::Now(); |
| 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; |
| |
| // 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", |
| GetNetworkStateNotifier().EffectiveType()); |
| } |
| |
| if (was_recorded_as_deferred_) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Blink.LazyLoad.CrossOriginFrames.VisibleAfterBeingDeferred", |
| GetNetworkStateNotifier().EffectiveType()); |
| } |
| } |
| |
| void LazyLoadFrameObserver::RecordMetricsOnLoadFinished() { |
| if (!time_when_first_load_finished_.is_null()) |
| return; |
| time_when_first_load_finished_ = base::TimeTicks::Now(); |
| 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_); |
| |
| base::TimeDelta visible_load_delay = |
| time_when_first_load_finished_ - time_when_first_visible_; |
| if (visible_load_delay < base::TimeDelta()) |
| visible_load_delay = base::TimeDelta(); |
| |
| switch (GetNetworkStateNotifier().EffectiveType()) { |
| 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 (GetNetworkStateNotifier().EffectiveType()) { |
| 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) { |
| element_->GetDocument().GetFrame()->Client()->DidObserveLazyLoadBehavior( |
| WebLocalFrameClient::LazyLoadBehavior::kDeferredFrame); |
| was_recorded_as_deferred_ = true; |
| } |
| } |
| |
| void LazyLoadFrameObserver::Trace(Visitor* visitor) { |
| visitor->Trace(element_); |
| visitor->Trace(lazy_load_intersection_observer_); |
| visitor->Trace(visibility_metrics_observer_); |
| } |
| |
| } // namespace blink |