Commit ff6a053e authored by Scott Little's avatar Scott Little Committed by Commit Bot

Add basic support for lazily loading below the fold cross-origin iframes

This CL adds basic support (behind a flag) for deferring the load of
cross-origin frames until the user scrolls near them, in order to reduce
network data usage, memory usage, and speed up the loading of other
content on the page.

When a frame is deferred, an IntersectionObserver is used to detect when
the deferred frame element is within a specific distance threshold of
the viewport and resume loading the frame. Currently, this
distance-from-viewport threshold is hardcoded, but in future CLs it will
be supplied from a field trial parameter.

Heuristics (i.e. tiny dimensions or offscreen position) are used to
recognize and immediately load frames that are likely used for analytics
or inter-frame communication, since those kinds of frames would be
broken by deferring them.

Once a lazily-loaded frame starts loading in, children within that
lazily-loaded frame won't be lazily loaded themselves. This will make it
possible for subresources in hidden frames to load that will never be
visible, as well as make it so that deferred frames that have multiple
layers of iframes inside them can load faster once they're near the
viewport or visible.

See the design docs below for more info.

LazyFrames design doc:
https://docs.google.com/document/d/1ITh7UqhmfirprVtjEtpfhga5Qyfoh78UkRmW8r3CntM/edit

LazyLoad design doc:
https://docs.google.com/document/d/1e8ZbVyUwgIkQMvJma3kKUDg8UUkLRRdANStqKuOIvHg/edit

Bug: 635105
Change-Id: I970221f527861298b58ed5b4662eb04019bc86ff
Reviewed-on: https://chromium-review.googlesource.com/979256
Commit-Queue: Scott Little <sclittle@chromium.org>
Reviewed-by: default avatarDaniel Cheng <dcheng@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Cr-Commit-Position: refs/heads/master@{#550317}
parent b6279a73
......@@ -399,6 +399,9 @@ void SetRuntimeFeaturesDefaultsAndUpdateFromArgs(
if (base::FeatureList::IsEnabled(features::kKeyboardLockAPI))
WebRuntimeFeatures::EnableFeatureFromString("KeyboardLock", true);
if (base::FeatureList::IsEnabled(features::kLazyFrameLoading))
WebRuntimeFeatures::EnableLazyFrameLoading(true);
// Enable explicitly enabled features, and then disable explicitly disabled
// ones.
for (const std::string& feature :
......
......@@ -155,6 +155,9 @@ const char kIsolateOriginsFieldTrialParamName[] = "OriginsList";
const base::Feature kKeyboardLockAPI{"KeyboardLockAPI",
base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kLazyFrameLoading{"LazyFrameLoading",
base::FEATURE_DISABLED_BY_DEFAULT};
// Enable lazy initialization of the media controls.
const base::Feature kLazyInitializeMediaControls{
"LazyInitializeMediaControls", base::FEATURE_ENABLED_BY_DEFAULT};
......
......@@ -46,6 +46,7 @@ CONTENT_EXPORT extern const base::Feature kImageCaptureAPI;
CONTENT_EXPORT extern const base::Feature kIsolateOrigins;
CONTENT_EXPORT extern const char kIsolateOriginsFieldTrialParamName[];
CONTENT_EXPORT extern const base::Feature kKeyboardLockAPI;
CONTENT_EXPORT extern const base::Feature kLazyFrameLoading;
CONTENT_EXPORT extern const base::Feature kLazyInitializeMediaControls;
CONTENT_EXPORT extern const base::Feature kLazyParseCSS;
CONTENT_EXPORT extern const base::Feature kLowPriorityIframes;
......
......@@ -87,6 +87,7 @@ class WebRuntimeFeatures {
BLINK_PLATFORM_EXPORT static void EnableGenericSensorExtraClasses(bool);
BLINK_PLATFORM_EXPORT static void EnableHeapCompaction(bool);
BLINK_PLATFORM_EXPORT static void EnableInputMultipleFieldsUI(bool);
BLINK_PLATFORM_EXPORT static void EnableLazyFrameLoading(bool);
BLINK_PLATFORM_EXPORT static void EnableLazyParseCSS(bool);
BLINK_PLATFORM_EXPORT static void EnableMediaCapture(bool);
BLINK_PLATFORM_EXPORT static void EnableMediaSession(bool);
......
......@@ -1829,6 +1829,7 @@ jumbo_source_set("unit_tests") {
"html/html_dimension_test.cc",
"html/html_embed_element_test.cc",
"html/html_frame_element_test.cc",
"html/html_frame_owner_element_test.cc",
"html/html_iframe_element_test.cc",
"html/html_image_element_test.cc",
"html/html_link_element_sizes_attribute_test.cc",
......
......@@ -61,6 +61,9 @@ class CORE_EXPORT FrameOwner : public GarbageCollectedMixin {
virtual bool IsDisplayNone() const = 0;
virtual AtomicString RequiredCsp() const = 0;
virtual const ParsedFeaturePolicy& ContainerPolicy() const = 0;
// Returns whether or not children of the owned frame should be lazily loaded.
virtual bool ShouldLazyLoadChildren() const = 0;
};
// TODO(dcheng): This class is an internal implementation detail of provisional
......@@ -100,6 +103,7 @@ class CORE_EXPORT DummyFrameOwner final
DEFINE_STATIC_LOCAL(ParsedFeaturePolicy, container_policy, ());
return container_policy;
}
bool ShouldLazyLoadChildren() const override { return false; }
private:
// Intentionally private to prevent redundant checks when the type is
......
......@@ -77,4 +77,11 @@ void RemoteFrameOwner::IntrinsicSizingInfoChanged() {
->IntrinsicSizingInfoChanged(intrinsic_sizing_info);
}
bool RemoteFrameOwner::ShouldLazyLoadChildren() const {
// Don't use lazy load for children inside an OOPIF, since there's a good
// chance that the parent FrameOwner was previously deferred by lazy load
// and then loaded in for whatever reason.
return false;
}
} // namespace blink
......@@ -57,6 +57,7 @@ class CORE_EXPORT RemoteFrameOwner final
const ParsedFeaturePolicy& ContainerPolicy() const override {
return container_policy_;
}
bool ShouldLazyLoadChildren() const final;
void SetBrowsingContextContainerName(const WebString& name) {
browsing_context_container_name_ = name;
......
......@@ -20,6 +20,8 @@
#include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
#include <limits>
#include "third_party/blink/public/platform/modules/fetch/fetch_api_request.mojom-shared.h"
#include "third_party/blink/renderer/bindings/core/v8/exception_messages.h"
#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
......@@ -31,6 +33,9 @@
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/frame/remote_frame_view.h"
#include "third_party/blink/renderer/core/geometry/dom_rect_read_only.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/layout/layout_embedded_content.h"
#include "third_party/blink/renderer/core/loader/document_loader.h"
#include "third_party/blink/renderer/core/loader/frame_load_request.h"
......@@ -40,6 +45,8 @@
#include "third_party/blink/renderer/core/timing/dom_window_performance.h"
#include "third_party/blink/renderer/core/timing/window_performance.h"
#include "third_party/blink/renderer/platform/heap/heap_allocator.h"
#include "third_party/blink/renderer/platform/length.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
namespace blink {
......@@ -52,6 +59,41 @@ PluginSet& PluginsPendingDispose() {
return set;
}
bool DoesParentAllowLazyLoadingChildren(Document& document) {
LocalFrame* containing_frame = document.GetFrame();
if (!containing_frame)
return true;
// If the embedding document has no owner, then by default allow lazy loading
// children.
FrameOwner* containing_frame_owner = containing_frame->Owner();
if (!containing_frame_owner)
return true;
return containing_frame_owner->ShouldLazyLoadChildren();
}
// 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) {
// 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;
return false;
}
} // namespace
SubframeLoadingDisabler::SubtreeRootSet&
......@@ -80,7 +122,9 @@ HTMLFrameOwnerElement::HTMLFrameOwnerElement(const QualifiedName& tag_name,
: HTMLElement(tag_name, document),
content_frame_(nullptr),
embedded_content_view_(nullptr),
sandbox_flags_(kSandboxNone) {}
sandbox_flags_(kSandboxNone),
should_lazy_load_children_(DoesParentAllowLazyLoadingChildren(document)) {
}
LayoutEmbeddedContent* HTMLFrameOwnerElement::GetLayoutEmbeddedContent() const {
// HTMLObjectElement and HTMLEmbedElement may return arbitrary layoutObjects
......@@ -96,6 +140,11 @@ void HTMLFrameOwnerElement::SetContentFrame(Frame& frame) {
DCHECK(!content_frame_ || content_frame_->Owner() != this);
// Disconnected frames should not be allowed to load.
DCHECK(isConnected());
// There should be no lazy load in progress since before SetContentFrame,
// |this| frame element should have been disconnected.
DCHECK(!lazy_load_intersection_observer_);
content_frame_ = &frame;
for (ContainerNode* node = this; node; node = node->ParentOrShadowHostNode())
......@@ -106,6 +155,10 @@ void HTMLFrameOwnerElement::ClearContentFrame() {
if (!content_frame_)
return;
// There should not be a lazy load in progress right now since any pending
// lazy load should have already been cancelled in DisconnectContentFrame.
DCHECK(!lazy_load_intersection_observer_);
DCHECK_EQ(content_frame_->Owner(), this);
content_frame_ = nullptr;
......@@ -117,6 +170,8 @@ void HTMLFrameOwnerElement::DisconnectContentFrame() {
if (!ContentFrame())
return;
CancelPendingLazyLoad();
// Removing a subframe that was still loading can impact the result of
// AllDescendantsAreComplete that is consulted by Document::ShouldComplete.
// Therefore we might need to re-check this after removing the subframe. The
......@@ -296,6 +351,7 @@ bool HTMLFrameOwnerElement::LoadOrRedirectSubframe(
UpdateContainerPolicy();
if (ContentFrame()) {
// TODO(sclittle): Support lazily loading frame navigations.
ContentFrame()->Navigate(GetDocument(), url, replace_current_item,
UserGestureStatus::kNone);
return true;
......@@ -336,14 +392,79 @@ bool HTMLFrameOwnerElement::LoadOrRedirectSubframe(
if (IsPlugin())
request.SetSkipServiceWorker(true);
child_frame->Loader().Load(FrameLoadRequest(&GetDocument(), request),
child_load_type);
if (RuntimeEnabledFeatures::LazyFrameLoadingEnabled() &&
should_lazy_load_children_ &&
// Only http:// or https:// URLs are eligible for lazy loading, excluding
// URLs like invalid or empty URLs, "about:blank", local file URLs, etc.
// that it doesn't make sense to lazily load.
url.ProtocolIsInHTTPFamily() &&
// Disallow lazy loading if javascript in the embedding document would be
// able to access the contents of the frame, since in those cases
// deferring the frame could break the page. Note that this check does not
// take any possible redirects of |url| into account.
!GetDocument().GetSecurityOrigin()->CanAccess(
SecurityOrigin::Create(url).get())) {
// Don't lazy load subresources inside a lazily loaded frame. This will make
// it possible for subresources in hidden frames to load that will
// never be visible, as well as make it so that deferred frames that have
// multiple layers of iframes inside them can load faster once they're near
// the viewport or visible.
should_lazy_load_children_ = false;
lazy_load_intersection_observer_ = IntersectionObserver::Create(
{Length(kLazyLoadRootMarginPx, kFixed)},
{std::numeric_limits<float>::min()}, &GetDocument(),
WTF::BindRepeating(&HTMLFrameOwnerElement::LoadIfHiddenOrNearViewport,
WrapWeakPersistent(this), request, child_load_type));
lazy_load_intersection_observer_->observe(this);
} else {
child_frame->Loader().Load(FrameLoadRequest(&GetDocument(), request),
child_load_type);
}
return true;
}
void HTMLFrameOwnerElement::LoadIfHiddenOrNearViewport(
const ResourceRequest& resource_request,
FrameLoadType frame_load_type,
const HeapVector<Member<IntersectionObserverEntry>>& entries) {
DCHECK(!entries.IsEmpty());
DCHECK_EQ(this, entries.back()->target());
if (!entries.back()->isIntersecting() &&
!IsFrameProbablyHidden(*entries.back()->boundingClientRect())) {
return;
}
// The content frame of this element should not have changed, since any
// pending lazy load should have been already been cancelled in
// DisconnectContentFrame() if the content frame changes.
DCHECK(ContentFrame());
// Note that calling FrameLoader::Load() causes this intersection observer to
// be disconnected.
ToLocalFrame(ContentFrame())
->Loader()
.Load(FrameLoadRequest(&GetDocument(), resource_request),
frame_load_type);
}
void HTMLFrameOwnerElement::CancelPendingLazyLoad() {
if (!lazy_load_intersection_observer_)
return;
lazy_load_intersection_observer_->disconnect();
lazy_load_intersection_observer_.Clear();
}
bool HTMLFrameOwnerElement::ShouldLazyLoadChildren() const {
return should_lazy_load_children_;
}
void HTMLFrameOwnerElement::Trace(blink::Visitor* visitor) {
visitor->Trace(content_frame_);
visitor->Trace(embedded_content_view_);
visitor->Trace(lazy_load_intersection_observer_);
HTMLElement::Trace(visitor);
FrameOwner::Trace(visitor);
}
......
......@@ -37,6 +37,8 @@ namespace blink {
class ExceptionState;
class Frame;
class IntersectionObserver;
class IntersectionObserverEntry;
class LayoutEmbeddedContent;
class WebPluginContainerImpl;
......@@ -110,10 +112,17 @@ class CORE_EXPORT HTMLFrameOwnerElement : public HTMLElement,
bool IsDisplayNone() const override { return !embedded_content_view_; }
AtomicString RequiredCsp() const override { return g_null_atom; }
const ParsedFeaturePolicy& ContainerPolicy() const override;
bool ShouldLazyLoadChildren() const final;
// For unit tests, manually trigger the UpdateContainerPolicy method.
void UpdateContainerPolicyForTests() { UpdateContainerPolicy(); }
void CancelPendingLazyLoad();
// TODO(sclittle): Make the root margins configurable via field trial
// params instead of just hardcoding the value here.
static constexpr int kLazyLoadRootMarginPx = 800;
virtual void Trace(blink::Visitor*);
protected:
......@@ -166,11 +175,19 @@ class CORE_EXPORT HTMLFrameOwnerElement : public HTMLElement,
return kReferrerPolicyDefault;
}
void LoadIfHiddenOrNearViewport(
const ResourceRequest&,
FrameLoadType,
const HeapVector<Member<IntersectionObserverEntry>>&);
Member<Frame> content_frame_;
Member<EmbeddedContentView> embedded_content_view_;
SandboxFlags sandbox_flags_;
ParsedFeaturePolicy container_policy_;
Member<IntersectionObserver> lazy_load_intersection_observer_;
bool should_lazy_load_children_;
};
DEFINE_ELEMENT_TYPE_CASTS(HTMLFrameOwnerElement, IsFrameOwnerElement());
......
......@@ -872,6 +872,9 @@ void FrameLoader::Load(const FrameLoadRequest& passed_request,
HistoryLoadType history_load_type) {
DCHECK(frame_->GetDocument());
if (HTMLFrameOwnerElement* element = frame_->DeprecatedLocalOwner())
element->CancelPendingLazyLoad();
if (IsBackForwardLoadType(frame_load_type) && !frame_->IsNavigationAllowed())
return;
......
......@@ -156,6 +156,10 @@ void WebRuntimeFeatures::EnableInputMultipleFieldsUI(bool enable) {
RuntimeEnabledFeatures::SetInputMultipleFieldsUIEnabled(enable);
}
void WebRuntimeFeatures::EnableLazyFrameLoading(bool enable) {
RuntimeEnabledFeatures::SetLazyFrameLoadingEnabled(enable);
}
void WebRuntimeFeatures::EnableLazyParseCSS(bool enable) {
RuntimeEnabledFeatures::SetLazyParseCSSEnabled(enable);
}
......
......@@ -613,6 +613,9 @@
{
name: "LayoutNGFragmentCaching",
},
{
name: "LazyFrameLoading",
},
{
name: "LazyInitializeMediaControls",
// This is enabled by features::kLazyInitializeMediaControls.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment