Bug 1542784 - Implement lazy loading for images. r=emilio,hsivonen
authorHiroyuki Ikezoe <hikezoe.birchill@mozilla.com>
Wed, 12 Feb 2020 21:31:48 +0000 (2020-02-12)
changeset 513623 e27886fc6a2a2b2915bebfbc6f22e494cd11404c
parent 513622 63cb9dca2a0d4740688ae1fccbdebf1ab314fdc7
child 513624 738e2a3729cf0cfcac567a70aedfdc0ed2b652a9
push id37118
push userrmaries@mozilla.com
push dateThu, 13 Feb 2020 03:57:45 +0000 (2020-02-13)
treeherdermozilla-central@2f6870dd1b99 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersemilio, hsivonen
bugs1542784
milestone75.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1542784 - Implement lazy loading for images. r=emilio,hsivonen Though with this initial implementation, we do create an IntersectionObserver only for the root document in each processes, once we found issues on this model, we can create an IntersectionObserver in each _document_. Depends on D61437 Differential Revision: https://phabricator.services.mozilla.com/D61438
dom/base/DOMIntersectionObserver.cpp
dom/base/DOMIntersectionObserver.h
dom/base/Document.cpp
dom/base/Document.h
dom/html/HTMLImageElement.cpp
dom/html/HTMLImageElement.h
testing/web-platform/meta/loading/lazyload/below-viewport-image-loading-lazy-load-event.tentative.html.ini
testing/web-platform/meta/loading/lazyload/disconnected-image-loading-lazy.tentative.html.ini
testing/web-platform/meta/loading/lazyload/image-loading-lazy-load-event.tentative.html.ini
testing/web-platform/meta/loading/lazyload/image-loading-lazy.tentative.html.ini
testing/web-platform/meta/loading/lazyload/move-element-and-scroll.tentative.html.ini
testing/web-platform/meta/loading/lazyload/not-rendered-below-viewport-image-loading-lazy.tentative.html.ini
testing/web-platform/meta/loading/lazyload/not-rendered-image-loading-lazy.tentative.html.ini
testing/web-platform/meta/loading/lazyload/original-base-url-applied-2-tentative.html.ini
testing/web-platform/meta/loading/lazyload/original-base-url-applied-tentative.html.ini
testing/web-platform/meta/loading/lazyload/original-crossorigin-applied-tentative.sub.html.ini
testing/web-platform/meta/loading/lazyload/original-referrer-policy-applied-tentative.sub.html.ini
testing/web-platform/meta/loading/lazyload/picture-loading-lazy.tentative.html.ini
testing/web-platform/meta/loading/lazyload/remove-element-and-scroll.tentative.html.ini
testing/web-platform/tests/loading/lazyload/image-loading-lazy-move-document.tentative.html
testing/web-platform/tests/loading/lazyload/resources/newwindow.html
--- a/dom/base/DOMIntersectionObserver.cpp
+++ b/dom/base/DOMIntersectionObserver.cpp
@@ -9,16 +9,17 @@
 #include "nsIFrame.h"
 #include "nsContentUtils.h"
 #include "nsLayoutUtils.h"
 #include "mozilla/PresShell.h"
 #include "mozilla/ServoBindings.h"
 #include "mozilla/dom/BrowserChild.h"
 #include "mozilla/dom/BrowsingContext.h"
 #include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/HTMLImageElement.h"
 #include "Units.h"
 
 namespace mozilla {
 namespace dom {
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
@@ -115,16 +116,35 @@ already_AddRefed<DOMIntersectionObserver
       return nullptr;
     }
     observer->mThresholds.AppendElement(thresh);
   }
 
   return observer.forget();
 }
 
+already_AddRefed<DOMIntersectionObserver>
+DOMIntersectionObserver::CreateLazyLoadObserver(nsPIDOMWindowInner* aOwner) {
+  RefPtr<DOMIntersectionObserver> observer = new DOMIntersectionObserver(
+      aOwner,
+      [](const Sequence<OwningNonNull<DOMIntersectionObserverEntry>>& entries) {
+        for (const auto& entry : entries) {
+          MOZ_ASSERT(entry->Target()->IsHTMLElement(nsGkAtoms::img));
+          if (entry->IsIntersecting()) {
+            static_cast<HTMLImageElement*>(entry->Target())
+                ->StopLazyLoadingAndStartLoadIfNeeded();
+          }
+        }
+      });
+
+  observer->mThresholds.AppendElement(std::numeric_limits<double>::min());
+
+  return observer.forget();
+}
+
 bool DOMIntersectionObserver::SetRootMargin(const nsAString& aString) {
   return Servo_IntersectionObserverRootMargin_Parse(&aString, &mRootMargin);
 }
 
 void DOMIntersectionObserver::GetRootMargin(DOMString& aRetVal) {
   nsString& retVal = aRetVal;
   Servo_IntersectionObserverRootMargin_ToString(&mRootMargin, &retVal);
 }
--- a/dom/base/DOMIntersectionObserver.h
+++ b/dom/base/DOMIntersectionObserver.h
@@ -80,16 +80,24 @@ class DOMIntersectionObserverEntry final
   }
 
 class DOMIntersectionObserver final : public nsISupports,
                                       public nsWrapperCache {
   virtual ~DOMIntersectionObserver() { Disconnect(); }
 
   typedef void (*NativeIntersectionObserverCallback)(
       const Sequence<OwningNonNull<DOMIntersectionObserverEntry>>& aEntries);
+  DOMIntersectionObserver(nsPIDOMWindowInner* aOwner,
+                          NativeIntersectionObserverCallback aCb)
+      : mOwner(aOwner),
+        mDocument(mOwner->GetExtantDoc()),
+        mCallback(aCb),
+        mConnected(false) {
+    MOZ_ASSERT(mOwner);
+  }
 
  public:
   DOMIntersectionObserver(already_AddRefed<nsPIDOMWindowInner>&& aOwner,
                           dom::IntersectionCallback& aCb)
       : mOwner(aOwner),
         mDocument(mOwner->GetExtantDoc()),
         mCallback(RefPtr<dom::IntersectionCallback>(&aCb)),
         mConnected(false) {}
@@ -126,16 +134,19 @@ class DOMIntersectionObserver final : pu
     return mCallback.as<RefPtr<dom::IntersectionCallback>>();
   }
 
   bool SetRootMargin(const nsAString& aString);
 
   void Update(Document* aDocument, DOMHighResTimeStamp time);
   MOZ_CAN_RUN_SCRIPT void Notify();
 
+  static already_AddRefed<DOMIntersectionObserver> CreateLazyLoadObserver(
+      nsPIDOMWindowInner* aOwner);
+
  protected:
   void Connect();
   void QueueIntersectionObserverEntry(Element* aTarget,
                                       DOMHighResTimeStamp time,
                                       const Maybe<nsRect>& aRootRect,
                                       const nsRect& aTargetRect,
                                       const Maybe<nsRect>& aIntersectionRect,
                                       double aIntersectionRatio);
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -2143,16 +2143,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyleSheetSetList)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mScriptLoader)
 
   DocumentOrShadowRoot::Traverse(tmp, cb);
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChannel)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLayoutHistoryState)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOnloadBlocker)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLazyLoadImageObserver)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMImplementation)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mImageMaps)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOrientationPendingPromise)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOriginalDocument)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCachedEncoder)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStateObjectCached)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentTimeline)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingAnimationTracker)
@@ -2250,16 +2251,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Do
     tmp->DisconnectChild(child);
     child->UnbindFromTree();
   }
 
   tmp->UnlinkOriginalDocumentIfStatic();
 
   tmp->mCachedRootElement = nullptr;  // Avoid a dangling pointer
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDisplayDocument)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mLazyLoadImageObserver)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDOMImplementation)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mImageMaps)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mCachedEncoder)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentTimeline)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingAnimationTracker)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mTemplateContentsOwner)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildrenCollection)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mImages);
@@ -14659,16 +14661,32 @@ void Document::NotifyIntersectionObserve
   }
   for (const auto& observer : observers) {
     if (observer) {
       observer->Notify();
     }
   }
 }
 
+DOMIntersectionObserver* Document::GetLazyLoadImageObserver() {
+  Document* rootDoc = nsContentUtils::GetRootDocument(this);
+  MOZ_ASSERT(rootDoc);
+
+  if (rootDoc->mLazyLoadImageObserver) {
+    return rootDoc->mLazyLoadImageObserver;
+  }
+
+  if (nsPIDOMWindowInner* inner = rootDoc->GetInnerWindow()) {
+    rootDoc->mLazyLoadImageObserver =
+        DOMIntersectionObserver::CreateLazyLoadObserver(inner);
+  }
+
+  return rootDoc->mLazyLoadImageObserver;
+}
+
 static CallState NotifyLayerManagerRecreatedCallback(Document& aDocument,
                                                      void*) {
   aDocument.NotifyLayerManagerRecreated();
   return CallState::Continue;
 }
 
 void Document::NotifyLayerManagerRecreated() {
   EnumerateActivityObservers(NotifyActivityChanged, nullptr);
--- a/dom/base/Document.h
+++ b/dom/base/Document.h
@@ -3607,16 +3607,18 @@ class Document : public nsINode,
   bool HasIntersectionObservers() const {
     return !mIntersectionObservers.IsEmpty();
   }
 
   void UpdateIntersectionObservations();
   void ScheduleIntersectionObserverNotification();
   MOZ_CAN_RUN_SCRIPT void NotifyIntersectionObservers();
 
+  DOMIntersectionObserver* GetLazyLoadImageObserver();
+
   // Dispatch a runnable related to the document.
   nsresult Dispatch(TaskCategory aCategory,
                     already_AddRefed<nsIRunnable>&& aRunnable) final;
 
   virtual nsISerialEventTarget* EventTargetFor(
       TaskCategory aCategory) const override;
 
   virtual AbstractThread* AbstractMainThreadFor(
@@ -4863,16 +4865,18 @@ class Document : public nsINode,
   // Weak reference to the scope object (aka the script global object)
   // that, unlike mScriptGlobalObject, is never unset once set. This
   // is a weak reference to avoid leaks due to circular references.
   nsWeakPtr mScopeObject;
 
   // Array of intersection observers
   nsTHashtable<nsPtrHashKey<DOMIntersectionObserver>> mIntersectionObservers;
 
+  RefPtr<DOMIntersectionObserver> mLazyLoadImageObserver;
+
   // Stack of fullscreen elements. When we request fullscreen we push the
   // fullscreen element onto this stack, and when we cancel fullscreen we
   // pop one off this stack, restoring the previous fullscreen state
   nsTArray<nsWeakPtr> mFullscreenStack;
 
   // The root of the doc tree in which this document is in. This is only
   // non-null when this document is in fullscreen mode.
   nsWeakPtr mFullscreenRoot;
--- a/dom/html/HTMLImageElement.cpp
+++ b/dom/html/HTMLImageElement.cpp
@@ -16,16 +16,17 @@
 #include "nsImageFrame.h"
 #include "nsIScriptContext.h"
 #include "nsContentUtils.h"
 #include "nsContainerFrame.h"
 #include "nsNodeInfoManager.h"
 #include "mozilla/MouseEvents.h"
 #include "nsContentPolicyUtils.h"
 #include "nsFocusManager.h"
+#include "mozilla/dom/DOMIntersectionObserver.h"
 #include "mozilla/dom/HTMLFormElement.h"
 #include "mozilla/dom/MutationEventBinding.h"
 #include "mozilla/dom/UserActivation.h"
 #include "nsAttrValueOrString.h"
 #include "imgLoader.h"
 #include "Image.h"
 
 // Responsive images!
@@ -108,16 +109,17 @@ class ImageLoadTask final : public Micro
   bool mUseUrgentStartForChannel;
 };
 
 HTMLImageElement::HTMLImageElement(
     already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
     : nsGenericHTMLElement(std::move(aNodeInfo)),
       mForm(nullptr),
       mInDocResponsiveContent(false),
+      mLazyLoading(false),
       mCurrentDensity(1.0) {
   // We start out broken
   AddStatesSilently(NS_EVENT_STATE_BROKEN);
 }
 
 HTMLImageElement::~HTMLImageElement() { DestroyImageLoadingContent(); }
 
 NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLImageElement, nsGenericHTMLElement,
@@ -304,16 +306,30 @@ nsresult HTMLImageElement::BeforeSetAttr
 
 nsresult HTMLImageElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
                                         const nsAttrValue* aValue,
                                         const nsAttrValue* aOldValue,
                                         nsIPrincipal* aMaybeScriptedPrincipal,
                                         bool aNotify) {
   nsAttrValueOrString attrVal(aValue);
 
+  if (aName == nsGkAtoms::loading && aNameSpaceID == kNameSpaceID_None) {
+    if (aValue &&
+        static_cast<HTMLImageElement::Loading>(aValue->GetEnumValue()) ==
+            Loading::Lazy &&
+        !ImageState().HasState(NS_EVENT_STATE_LOADING)) {
+      SetLazyLoading();
+    } else if (aOldValue &&
+               static_cast<HTMLImageElement::Loading>(
+                   aOldValue->GetEnumValue()) == Loading::Lazy &&
+               !ImageState().HasState(NS_EVENT_STATE_LOADING)) {
+      StopLazyLoadingAndStartLoadIfNeeded();
+    }
+  }
+
   if (aValue) {
     AfterMaybeChangeAttr(aNameSpaceID, aName, attrVal, aOldValue,
                          aMaybeScriptedPrincipal, true, aNotify);
   }
 
   if (aNameSpaceID == kNameSpaceID_None && mForm &&
       (aName == nsGkAtoms::name || aName == nsGkAtoms::id) && aValue &&
       !aValue->IsEmptyString()) {
@@ -404,17 +420,17 @@ void HTMLImageElement::AfterMaybeChangeA
         this, aValue.String(), aMaybeScriptedPrincipal);
 
     if (InResponsiveMode()) {
       if (mResponsiveSelector && mResponsiveSelector->Content() == this) {
         mResponsiveSelector->SetDefaultSource(aValue.String(),
                                               mSrcTriggeringPrincipal);
       }
       QueueImageLoadTask(true);
-    } else if (aNotify && OwnerDoc()->ShouldLoadImages()) {
+    } else if (aNotify && ShouldLoadImage()) {
       // If aNotify is false, we are coming from the parser or some such place;
       // we'll get bound after all the attributes have been set, so we'll do the
       // sync image load from BindToTree. Skip the LoadImage call in that case.
 
       // Note that this sync behavior is partially removed from the spec, bug
       // 1076583
 
       // A hack to get animations to reset. See bug 594771.
@@ -462,17 +478,17 @@ void HTMLImageElement::AfterMaybeChangeA
     // Mark channel as urgent-start before load image if the image load is
     // initaiated by a user interaction.
     mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
 
     if (InResponsiveMode()) {
       // per spec, full selection runs when this changes, even though
       // it doesn't directly affect the source selection
       QueueImageLoadTask(true);
-    } else if (OwnerDoc()->ShouldLoadImages()) {
+    } else if (ShouldLoadImage()) {
       // Bug 1076583 - We still use the older synchronous algorithm in
       // non-responsive mode. Force a new load of the image with the
       // new cross origin policy
       ForceReload(aNotify, IgnoreErrors());
     }
   }
 }
 
@@ -553,17 +569,17 @@ nsresult HTMLImageElement::BindToTree(Bi
 
     // We still act synchronously for the non-responsive case (Bug
     // 1076583), but still need to delay if it is unsafe to run
     // script.
 
     // If loading is temporarily disabled, don't even launch MaybeLoadImage.
     // Otherwise MaybeLoadImage may run later when someone has reenabled
     // loading.
-    if (LoadingEnabled() && aContext.OwnerDoc().ShouldLoadImages()) {
+    if (LoadingEnabled() && ShouldLoadImage()) {
       nsContentUtils::AddScriptRunner(
           NewRunnableMethod<bool>("dom::HTMLImageElement::MaybeLoadImage", this,
                                   &HTMLImageElement::MaybeLoadImage, false));
     }
   }
 
   return rv;
 }
@@ -627,31 +643,27 @@ void HTMLImageElement::MaybeLoadImage(bo
 
 EventStates HTMLImageElement::IntrinsicState() const {
   return nsGenericHTMLElement::IntrinsicState() |
          nsImageLoadingContent::ImageState();
 }
 
 void HTMLImageElement::NodeInfoChanged(Document* aOldDoc) {
   nsGenericHTMLElement::NodeInfoChanged(aOldDoc);
+
+  if (mLazyLoading) {
+    aOldDoc->GetLazyLoadImageObserver()->Unobserve(*this);
+    mLazyLoading = false;
+    SetLazyLoading();
+  }
+
   // Force reload image if adoption steps are run.
   // If loading is temporarily disabled, don't even launch script runner.
   // Otherwise script runner may run later when someone has reenabled loading.
-  if (LoadingEnabled() && OwnerDoc()->ShouldLoadImages()) {
-    // Use script runner for the case the adopt is from appendChild.
-    // Bug 1076583 - We still behave synchronously in the non-responsive case
-    nsContentUtils::AddScriptRunner(
-        (InResponsiveMode())
-            ? NewRunnableMethod<bool>(
-                  "dom::HTMLImageElement::QueueImageLoadTask", this,
-                  &HTMLImageElement::QueueImageLoadTask, true)
-            : NewRunnableMethod<bool>("dom::HTMLImageElement::MaybeLoadImage",
-                                      this, &HTMLImageElement::MaybeLoadImage,
-                                      true));
-  }
+  StartLoadingIfNeeded();
 }
 
 // static
 already_AddRefed<HTMLImageElement> HTMLImageElement::Image(
     const GlobalObject& aGlobal, const Optional<uint32_t>& aWidth,
     const Optional<uint32_t>& aHeight, ErrorResult& aError) {
   nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports());
   Document* doc;
@@ -729,17 +741,17 @@ nsresult HTMLImageElement::CopyInnerTo(H
 
   if (!destIsStatic) {
     // In SetAttr (called from nsGenericHTMLElement::CopyInnerTo), aDest skipped
     // doing the image load because we passed in false for aNotify.  But we
     // really do want it to do the load, so set it up to happen once the cloning
     // reaches a stable state.
     if (!aDest->InResponsiveMode() &&
         aDest->HasAttr(kNameSpaceID_None, nsGkAtoms::src) &&
-        aDest->OwnerDoc()->ShouldLoadImages()) {
+        aDest->ShouldLoadImage()) {
       // Mark channel as urgent-start before load image if the image load is
       // initaiated by a user interaction.
       mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
 
       nsContentUtils::AddScriptRunner(NewRunnableMethod<bool>(
           "dom::HTMLImageElement::MaybeLoadImage", aDest,
           &HTMLImageElement::MaybeLoadImage, false));
     }
@@ -795,17 +807,17 @@ void HTMLImageElement::ClearForm(bool aR
 
   UnsetFlags(ADDED_TO_FORM);
   mForm = nullptr;
 }
 
 void HTMLImageElement::QueueImageLoadTask(bool aAlwaysLoad) {
   // If loading is temporarily disabled, we don't want to queue tasks
   // that may then run when loading is re-enabled.
-  if (!LoadingEnabled() || !OwnerDoc()->ShouldLoadImages()) {
+  if (!LoadingEnabled() || !ShouldLoadImage()) {
     return;
   }
 
   // Ensure that we don't overwrite a previous load request that requires
   // a complete load to occur.
   bool alwaysLoad = aAlwaysLoad;
   if (mPendingImageLoadTask) {
     alwaysLoad = alwaysLoad || mPendingImageLoadTask->AlwaysLoad();
@@ -1224,10 +1236,65 @@ void HTMLImageElement::DestroyContent() 
 
   nsGenericHTMLElement::DestroyContent();
 }
 
 void HTMLImageElement::MediaFeatureValuesChanged() {
   QueueImageLoadTask(false);
 }
 
+bool HTMLImageElement::ShouldLoadImage() const {
+  return OwnerDoc()->ShouldLoadImages() && !mLazyLoading;
+}
+
+void HTMLImageElement::SetLazyLoading() {
+  if (mLazyLoading) {
+    return;
+  }
+
+  // If scripting is disabled don't do lazy load.
+  // https://whatpr.org/html/3752/images.html#updating-the-image-data
+  if (!OwnerDoc()->IsScriptEnabled()) {
+    return;
+  }
+
+  // There (maybe) is a race condition that we have no LazyLoadImageObserver
+  // when the root document has been removed from the docshell.
+  // In the case we don't need to worry about lazy-loading.
+  if (DOMIntersectionObserver* lazyLoadObserver =
+          OwnerDoc()->GetLazyLoadImageObserver()) {
+    lazyLoadObserver->Observe(*this);
+    mLazyLoading = true;
+  }
+}
+
+void HTMLImageElement::StartLoadingIfNeeded() {
+  if (LoadingEnabled() && ShouldLoadImage()) {
+    // Use script runner for the case the adopt is from appendChild.
+    // Bug 1076583 - We still behave synchronously in the non-responsive case
+    nsContentUtils::AddScriptRunner(
+        (InResponsiveMode())
+            ? NewRunnableMethod<bool>(
+                  "dom::HTMLImageElement::QueueImageLoadTask", this,
+                  &HTMLImageElement::QueueImageLoadTask, true)
+            : NewRunnableMethod<bool>("dom::HTMLImageElement::MaybeLoadImage",
+                                      this, &HTMLImageElement::MaybeLoadImage,
+                                      true));
+  }
+}
+
+void HTMLImageElement::StopLazyLoadingAndStartLoadIfNeeded() {
+  if (!mLazyLoading) {
+    return;
+  }
+  mLazyLoading = false;
+
+  DOMIntersectionObserver* lazyLoadObserver =
+      OwnerDoc()->GetLazyLoadImageObserver();
+  MOZ_ASSERT(lazyLoadObserver);
+
+  lazyLoadObserver->Unobserve(*this);
+
+  StartLoadingIfNeeded();
+}
+
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/html/HTMLImageElement.h
+++ b/dom/html/HTMLImageElement.h
@@ -254,16 +254,18 @@ class HTMLImageElement final : public ns
    * further <source> or <img> tags would be considered.
    */
   static bool SelectSourceForTagWithAttrs(
       Document* aDocument, bool aIsSourceTag, const nsAString& aSrcAttr,
       const nsAString& aSrcsetAttr, const nsAString& aSizesAttr,
       const nsAString& aTypeAttr, const nsAString& aMediaAttr,
       nsAString& aResult);
 
+  void StopLazyLoadingAndStartLoadIfNeeded();
+
  protected:
   virtual ~HTMLImageElement();
 
   // Queues a task to run LoadSelectedImage pending stable state.
   //
   // Pending Bug 1076583 this is only used by the responsive image
   // algorithm (InResponsiveMode()) -- synchronous actions when just
   // using img.src will bypass this, and update source and kick off
@@ -374,17 +376,28 @@ class HTMLImageElement final : public ns
    * @param aNotify Whether we plan to notify document observers.
    */
   void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName,
                             const nsAttrValueOrString& aValue,
                             const nsAttrValue* aOldValue,
                             nsIPrincipal* aMaybeScriptedPrincipal,
                             bool aValueMaybeChanged, bool aNotify);
 
+  bool ShouldLoadImage() const;
+
+  // Set this image as a lazy load image due to loading="lazy".
+  void SetLazyLoading();
+
+  void StartLoadingIfNeeded();
+
   bool mInDocResponsiveContent;
+
+  // Represents the image is deferred loading until this element gets visible.
+  bool mLazyLoading;
+
   RefPtr<ImageLoadTask> mPendingImageLoadTask;
   nsCOMPtr<nsIPrincipal> mSrcTriggeringPrincipal;
   nsCOMPtr<nsIPrincipal> mSrcsetTriggeringPrincipal;
 
   // Last URL that was attempted to load by this element.
   nsCOMPtr<nsIURI> mLastSelectedSource;
   // Last pixel density that was selected.
   double mCurrentDensity;
deleted file mode 100644
--- a/testing/web-platform/meta/loading/lazyload/below-viewport-image-loading-lazy-load-event.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[below-viewport-image-loading-lazy-load-event.tentative.html]
-  [Below-viewport loading=lazy images do not block the window load event when scrolled into viewport]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/loading/lazyload/disconnected-image-loading-lazy.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[disconnected-image-loading-lazy.tentative.html]
-  [loading=lazy for disconnected image]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/loading/lazyload/image-loading-lazy-load-event.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[image-loading-lazy-load-event.tentative.html]
-  [In-viewport loading=lazy images do not block the window load event]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/loading/lazyload/image-loading-lazy.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[image-loading-lazy.tentative.html]
-  [Images with loading='lazy' load only when in the viewport]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/loading/lazyload/move-element-and-scroll.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[move-element-and-scroll.tentative.html]
-  [Test that <img> below viewport is not loaded when moved to another document and then scrolled to]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/loading/lazyload/not-rendered-below-viewport-image-loading-lazy.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[not-rendered-below-viewport-image-loading-lazy.tentative.html]
-  [Below-viewport loading=lazy not-rendered images should never load, even when scrolled into view]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/loading/lazyload/not-rendered-image-loading-lazy.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[not-rendered-image-loading-lazy.tentative.html]
-  [In-viewport loading=lazy not-rendered images should never load]
-    expected: FAIL
-
--- a/testing/web-platform/meta/loading/lazyload/original-base-url-applied-2-tentative.html.ini
+++ b/testing/web-platform/meta/loading/lazyload/original-base-url-applied-2-tentative.html.ini
@@ -1,4 +1,5 @@
 [original-base-url-applied-2-tentative.html]
   [Deferred images with loading='lazy' use the original base URL specified at the parse time]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1613277
 
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/loading/lazyload/original-base-url-applied-tentative.html.ini
@@ -0,0 +1,5 @@
+[original-base-url-applied-tentative.html]
+  [Test that when deferred img is loaded, it uses the base URL computed at parse time.]
+    expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1613277
+
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/loading/lazyload/original-crossorigin-applied-tentative.sub.html.ini
@@ -0,0 +1,5 @@
+[original-crossorigin-applied-tentative.sub.html]
+  [Test that when deferred image is loaded, it uses the crossorigin attribute specified at parse time.]
+    expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1613277
+
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/loading/lazyload/original-referrer-policy-applied-tentative.sub.html.ini
@@ -0,0 +1,5 @@
+[original-referrer-policy-applied-tentative.sub.html]
+  [Test that when deferred img is loaded, it uses the referrer-policy specified at parse time.]
+    expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1613277
+
deleted file mode 100644
--- a/testing/web-platform/meta/loading/lazyload/picture-loading-lazy.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[picture-loading-lazy.tentative.html]
-  [Test that the loading=lazy <picture> element below viewport was deferred, on document load.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/loading/lazyload/remove-element-and-scroll.tentative.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[remove-element-and-scroll.tentative.html]
-  [Test that <img> below viewport is not loaded when removed from the document and then scrolled to]
-    expected: FAIL
-
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/loading/lazyload/image-loading-lazy-move-document.tentative.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<head>
+<title>Moving loading='lazy' image into another top level document</title>
+<link rel="help" href="https://github.com/scott-little/lazyload">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+
+<!--
+Marked as tentative until https://github.com/whatwg/html/pull/3752 is landed.
+-->
+
+<div style="height:1000vh;"></div>
+<img loading="lazy"
+     src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC">
+<script>
+promise_test(async t => {
+  let image_loaded = false;
+  const img = document.querySelector("img");
+  img.addEventListener("load", () => { image_loaded = true; });
+
+  await new Promise(resolve => window.addEventListener("load", resolve));
+
+  assert_false(image_loaded,
+               "lazy-load image shouldn't be loaded yet");
+
+  const anotherWin = window.open("resources/newwindow.html");
+
+  await new Promise(resolve => anotherWin.addEventListener("load", resolve));
+
+  anotherWin.document.body.appendChild(img);
+
+  assert_false(image_loaded,
+               "lazy-load image shouldn't be loaded yet");
+
+  img.scrollIntoView();
+
+  await new Promise(resolve => img.addEventListener("load", resolve));
+  assert_true(img.complete,
+              "Now the lazy-load image should be loaded");
+});
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/loading/lazyload/resources/newwindow.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<div style="height:1000vh;"></div>