Bug 1067345 - Part 3 - Implement speculative load operations for <picture> and <img srcset>
authorJohn Schoenick <jschoenick@mozilla.com>
Wed, 10 Dec 2014 18:54:00 -0500
changeset 229467 b64c085b06e1446de958cbd0186d2900c50026fc
parent 229466 c1b3abf6bb988443bf0670035219ec4c585e0bee
child 229468 9c39680606e8d0a808c64b6fdd4fca999e392058
push id28287
push userryanvm@gmail.com
push dateTue, 17 Feb 2015 20:08:22 +0000
treeherdermozilla-central@b6c56fab513d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1067345
milestone38.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 1067345 - Part 3 - Implement speculative load operations for <picture> and <img srcset>
dom/base/nsDocument.cpp
dom/base/nsDocument.h
dom/base/nsIDocument.h
parser/html/nsHtml5SpeculativeLoad.cpp
parser/html/nsHtml5SpeculativeLoad.h
parser/html/nsHtml5TreeBuilderCppSupplement.h
parser/html/nsHtml5TreeOpExecutor.cpp
parser/html/nsHtml5TreeOpExecutor.h
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -141,16 +141,17 @@
 #include "mozilla/dom/StyleSheetApplicableStateChangeEvent.h"
 #include "nsJSUtils.h"
 #include "nsFrameLoader.h"
 #include "nsEscape.h"
 #include "nsObjectLoadingContent.h"
 #include "nsHtml5TreeOpExecutor.h"
 #include "mozilla/dom/HTMLLinkElement.h"
 #include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/HTMLImageElement.h"
 #include "mozilla/dom/MediaSource.h"
 
 #include "mozAutoDocUpdate.h"
 #include "nsGlobalWindow.h"
 #include "mozilla/dom/EncodingUtils.h"
 #include "nsDOMNavigationTiming.h"
 
 #include "nsSMILAnimationController.h"
@@ -1601,16 +1602,19 @@ nsDocument::nsDocument(const char* aCont
 
   if (!gCspPRLog)
     gCspPRLog = PR_NewLogModule("CSP");
 #endif
 
   // Start out mLastStyleSheetSet as null, per spec
   SetDOMStringToNull(mLastStyleSheetSet);
 
+  // void state used to differentiate an empty source from an unselected source
+  mPreloadPictureFoundSource.SetIsVoid(true);
+
   if (!sProcessingStack) {
     sProcessingStack.emplace();
     // Add the base queue sentinel to the processing stack.
     sProcessingStack->AppendElement((CustomElementData*) nullptr);
   }
 }
 
 static PLDHashOperator
@@ -9665,16 +9669,97 @@ FireOrClearDelayedEvents(nsTArray<nsCOMP
                     aDocuments[i]->GetInnerWindow()->IsCurrentInnerWindow();
         shell->FireOrClearDelayedEvents(fire);
       }
     }
   }
 }
 
 void
+nsDocument::PreloadPictureOpened()
+{
+  mPreloadPictureDepth++;
+}
+
+void
+nsDocument::PreloadPictureClosed()
+{
+  mPreloadPictureDepth--;
+  if (mPreloadPictureDepth == 0) {
+    mPreloadPictureFoundSource.SetIsVoid(true);
+  } else {
+    MOZ_ASSERT(mPreloadPictureDepth >= 0);
+  }
+}
+
+void
+nsDocument::PreloadPictureImageSource(const nsAString& aSrcsetAttr,
+                                      const nsAString& aSizesAttr,
+                                      const nsAString& aTypeAttr,
+                                      const nsAString& aMediaAttr)
+{
+  // Nested pictures are not valid syntax, so while we'll eventually load them,
+  // it's not worth tracking sources mixed between nesting levels to preload
+  // them effectively.
+  if (mPreloadPictureDepth == 1 && mPreloadPictureFoundSource.IsVoid()) {
+    // <picture> selects the first matching source, so if this returns a URI we
+    // needn't consider new sources until a new <picture> is encountered.
+    bool found =
+      HTMLImageElement::SelectSourceForTagWithAttrs(this, true, NullString(),
+                                                    aSrcsetAttr, aSizesAttr,
+                                                    aTypeAttr, aMediaAttr,
+                                                    mPreloadPictureFoundSource);
+    if (found && mPreloadPictureFoundSource.IsVoid()) {
+      // Found an empty source, which counts
+      mPreloadPictureFoundSource.SetIsVoid(false);
+    }
+  }
+}
+
+already_AddRefed<nsIURI>
+nsDocument::ResolvePreloadImage(nsIURI *aBaseURI,
+                                const nsAString& aSrcAttr,
+                                const nsAString& aSrcsetAttr,
+                                const nsAString& aSizesAttr)
+{
+  nsString sourceURL;
+  if (mPreloadPictureDepth == 1 && !mPreloadPictureFoundSource.IsVoid()) {
+    // We're in a <picture> element and found a URI from a source previous to
+    // this image, use it.
+    sourceURL = mPreloadPictureFoundSource;
+  } else {
+    // Otherwise try to use this <img> as a source
+    HTMLImageElement::SelectSourceForTagWithAttrs(this, false, aSrcAttr,
+                                                  aSrcsetAttr, aSizesAttr,
+                                                  NullString(), NullString(),
+                                                  sourceURL);
+  }
+
+  // Empty sources are not loaded by <img> (i.e. not resolved to the baseURI)
+  if (sourceURL.IsEmpty()) {
+    return nullptr;
+  }
+
+  // Construct into URI using passed baseURI (the parser may know of base URI
+  // changes that have not reached us)
+  nsresult rv;
+  nsCOMPtr<nsIURI> uri;
+  rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri), sourceURL,
+                                                 this, aBaseURI);
+  if (NS_FAILED(rv)) {
+    return nullptr;
+  }
+
+  // We don't clear mPreloadPictureFoundSource because subsequent <img> tags in
+  // this this <picture> share the same <sources> (though this is not valid per
+  // spec)
+  return uri.forget();
+}
+
+void
 nsDocument::MaybePreLoadImage(nsIURI* uri, const nsAString &aCrossOriginAttr,
                               ReferrerPolicy aReferrerPolicy)
 {
   // Early exit if the img is already present in the img-cache
   // which indicates that the "real" load has already started and
   // that we shouldn't preload it.
   int16_t blockingStatus;
   if (nsContentUtils::IsImageInCache(uri, static_cast<nsIDocument *>(this)) ||
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -1097,16 +1097,31 @@ public:
   }
 
   nsresult CloneDocHelper(nsDocument* clone) const;
 
   void MaybeInitializeFinalizeFrameLoaders();
 
   void MaybeEndOutermostXBLUpdate();
 
+  virtual void PreloadPictureOpened() MOZ_OVERRIDE;
+  virtual void PreloadPictureClosed() MOZ_OVERRIDE;
+
+  virtual void
+    PreloadPictureImageSource(const nsAString& aSrcsetAttr,
+                              const nsAString& aSizesAttr,
+                              const nsAString& aTypeAttr,
+                              const nsAString& aMediaAttr) MOZ_OVERRIDE;
+
+  virtual already_AddRefed<nsIURI>
+    ResolvePreloadImage(nsIURI *aBaseURI,
+                        const nsAString& aSrcAttr,
+                        const nsAString& aSrcsetAttr,
+                        const nsAString& aSizesAttr) MOZ_OVERRIDE;
+
   virtual void MaybePreLoadImage(nsIURI* uri,
                                  const nsAString &aCrossOriginAttr,
                                  ReferrerPolicy aReferrerPolicy) MOZ_OVERRIDE;
   virtual void ForgetImagePreload(nsIURI* aURI) MOZ_OVERRIDE;
 
   virtual void PreloadStyle(nsIURI* uri, const nsAString& charset,
                             const nsAString& aCrossOriginAttr,
                             ReferrerPolicy aReferrerPolicy) MOZ_OVERRIDE;
@@ -1762,16 +1777,22 @@ private:
   nsExternalResourceMap mExternalResourceMap;
 
   // All images in process of being preloaded.  This is a hashtable so
   // we can remove them as the real image loads start; that way we
   // make sure to not keep the image load going when no one cares
   // about it anymore.
   nsRefPtrHashtable<nsURIHashKey, imgIRequest> mPreloadingImages;
 
+  // Current depth of picture elements from parser
+  int32_t mPreloadPictureDepth;
+
+  // Set if we've found a URL for the current picture
+  nsString mPreloadPictureFoundSource;
+
   nsRefPtr<mozilla::dom::DOMImplementation> mDOMImplementation;
 
   nsRefPtr<nsContentList> mImageMaps;
 
   nsCString mScrollToRef;
   uint8_t mScrolledToRefAlready : 1;
   uint8_t mChangeScrollPosWhenScrollingToRef : 1;
 
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -141,19 +141,20 @@ typedef CallbackObjectHolder<NodeFilter,
 struct FullScreenOptions {
   FullScreenOptions() { }
   nsRefPtr<gfx::VRHMDInfo> mVRHMDDevice;
 };
 
 } // namespace dom
 } // namespace mozilla
 
+// 137c6144-513e-4edf-840e-5e3156638ed6
 #define NS_IDOCUMENT_IID \
-{ 0xf63d2f6e, 0xd1c1, 0x49b9, \
- { 0x88, 0x26, 0xd5, 0x9e, 0x5d, 0x72, 0x2a, 0x42 } }
+{ 0x137c6144, 0x513e, 0x4edf, \
+  { 0x84, 0x0e, 0x5e, 0x31, 0x56, 0x63, 0x8e, 0xd6 } }
 
 // Enum for requesting a particular type of document when creating a doc
 enum DocumentFlavor {
   DocumentFlavorLegacyGuess, // compat with old code until made HTML5-compliant
   DocumentFlavorHTML, // HTMLDocument with HTMLness bit set to true
   DocumentFlavorSVG, // SVGDocument
   DocumentFlavorPlain, // Just a Document
 };
@@ -1920,16 +1921,49 @@ public:
    */
   nsIDocument* GetOriginalDocument()
   {
     MOZ_ASSERT(!mOriginalDocument || !mOriginalDocument->GetOriginalDocument());
     return mOriginalDocument;
   }
 
   /**
+   * These are called by the parser as it encounters <picture> tags, the end of
+   * said tags, and possible picture <source srcset> sources respectively. These
+   * are used to inform ResolvePreLoadImage() calls.  Unset attributes are
+   * expected to be marked void.
+   *
+   * NOTE that the parser does not attempt to track the current picture nesting
+   * level or whether the given <source> tag is within a picture -- it is only
+   * guaranteed to order these calls properly with respect to
+   * ResolvePreLoadImage.
+   */
+
+  virtual void PreloadPictureOpened() = 0;
+
+  virtual void PreloadPictureClosed() = 0;
+
+  virtual void PreloadPictureImageSource(const nsAString& aSrcsetAttr,
+                                         const nsAString& aSizesAttr,
+                                         const nsAString& aTypeAttr,
+                                         const nsAString& aMediaAttr) = 0;
+
+  /**
+   * Called by the parser to resolve an image for preloading. The parser will
+   * call the PreloadPicture* functions to inform us of possible <picture>
+   * nesting and possible sources, which are used to inform URL selection
+   * responsive <picture> or <img srcset> images.  Unset attributes are expected
+   * to be marked void.
+   */
+  virtual already_AddRefed<nsIURI>
+    ResolvePreloadImage(nsIURI *aBaseURI,
+                        const nsAString& aSrcAttr,
+                        const nsAString& aSrcsetAttr,
+                        const nsAString& aSizesAttr) = 0;
+  /**
    * Called by nsParser to preload images. Can be removed and code moved
    * to nsPreloadURIs::PreloadURIs() in file nsParser.cpp whenever the
    * parser-module is linked with gklayout-module.  aCrossOriginAttr should
    * be a void string if the attr is not present.
    */
   virtual void MaybePreLoadImage(nsIURI* uri,
                                  const nsAString& aCrossOriginAttr,
                                  ReferrerPolicy aReferrerPolicy) = 0;
--- a/parser/html/nsHtml5SpeculativeLoad.cpp
+++ b/parser/html/nsHtml5SpeculativeLoad.cpp
@@ -26,17 +26,27 @@ nsHtml5SpeculativeLoad::Perform(nsHtml5T
   switch (mOpCode) {
     case eSpeculativeLoadBase:
       aExecutor->SetSpeculationBase(mUrl);
       break;
     case eSpeculativeLoadMetaReferrer:
       aExecutor->SetSpeculationReferrerPolicy(mMetaReferrerPolicy);
       break;
     case eSpeculativeLoadImage:
-      aExecutor->PreloadImage(mUrl, mCrossOrigin);
+      aExecutor->PreloadImage(mUrl, mCrossOrigin, mSrcset, mSizes);
+      break;
+    case eSpeculativeLoadOpenPicture:
+      aExecutor->PreloadOpenPicture();
+      break;
+    case eSpeculativeLoadEndPicture:
+      aExecutor->PreloadEndPicture();
+      break;
+    case eSpeculativeLoadPictureSource:
+      aExecutor->PreloadPictureSource(mSrcset, mSizes, mTypeOrCharsetSource,
+                                      mMedia);
       break;
     case eSpeculativeLoadScript:
       aExecutor->PreloadScript(mUrl, mCharset, mTypeOrCharsetSource,
                                mCrossOrigin, false);
       break;
     case eSpeculativeLoadScriptFromHead:
       aExecutor->PreloadScript(mUrl, mCharset, mTypeOrCharsetSource,
                                mCrossOrigin, true);
--- a/parser/html/nsHtml5SpeculativeLoad.h
+++ b/parser/html/nsHtml5SpeculativeLoad.h
@@ -12,28 +12,31 @@ class nsHtml5TreeOpExecutor;
 
 enum eHtml5SpeculativeLoad {
 #ifdef DEBUG
   eSpeculativeLoadUninitialized,
 #endif
   eSpeculativeLoadBase,
   eSpeculativeLoadMetaReferrer,
   eSpeculativeLoadImage,
+  eSpeculativeLoadOpenPicture,
+  eSpeculativeLoadEndPicture,
+  eSpeculativeLoadPictureSource,
   eSpeculativeLoadScript,
   eSpeculativeLoadScriptFromHead,
   eSpeculativeLoadStyle,
   eSpeculativeLoadManifest,
   eSpeculativeLoadSetDocumentCharset
 };
 
 class nsHtml5SpeculativeLoad {
   public:
     nsHtml5SpeculativeLoad();
     ~nsHtml5SpeculativeLoad();
-    
+
     inline void InitBase(const nsAString& aUrl)
     {
       NS_PRECONDITION(mOpCode == eSpeculativeLoadUninitialized,
                       "Trying to reinitialize a speculative load!");
       mOpCode = eSpeculativeLoadBase;
       mUrl.Assign(aUrl);
     }
 
@@ -41,23 +44,62 @@ class nsHtml5SpeculativeLoad {
       NS_PRECONDITION(mOpCode == eSpeculativeLoadUninitialized,
                       "Trying to reinitialize a speculative load!");
       mOpCode = eSpeculativeLoadMetaReferrer;
       mMetaReferrerPolicy.Assign(
         nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>(aReferrerPolicy));
     }
 
     inline void InitImage(const nsAString& aUrl,
-                          const nsAString& aCrossOrigin)
+                          const nsAString& aCrossOrigin,
+                          const nsAString& aSrcset,
+                          const nsAString& aSizes)
     {
       NS_PRECONDITION(mOpCode == eSpeculativeLoadUninitialized,
                       "Trying to reinitialize a speculative load!");
       mOpCode = eSpeculativeLoadImage;
       mUrl.Assign(aUrl);
       mCrossOrigin.Assign(aCrossOrigin);
+      mSrcset.Assign(aSrcset);
+      mSizes.Assign(aSizes);
+    }
+
+    // <picture> elements have multiple <source> nodes followed by an <img>,
+    // where we use the first valid source, which may be the img. Because we
+    // can't determine validity at this point without parsing CSS and getting
+    // main thread state, we push preload operations for picture pushed and
+    // popped, so that the target of the preload ops can determine what picture
+    // and nesting level each source/img from the main preloading code exists
+    // at.
+    inline void InitOpenPicture()
+    {
+      NS_PRECONDITION(mOpCode == eSpeculativeLoadUninitialized,
+                      "Trying to reinitialize a speculative load!");
+      mOpCode = eSpeculativeLoadOpenPicture;
+    }
+
+    inline void InitEndPicture()
+    {
+      NS_PRECONDITION(mOpCode == eSpeculativeLoadUninitialized,
+                      "Trying to reinitialize a speculative load!");
+      mOpCode = eSpeculativeLoadEndPicture;
+    }
+
+    inline void InitPictureSource(const nsAString& aSrcset,
+                                  const nsAString& aSizes,
+                                  const nsAString& aType,
+                                  const nsAString& aMedia)
+    {
+      NS_PRECONDITION(mOpCode == eSpeculativeLoadUninitialized,
+                      "Trying to reinitialize a speculative load!");
+      mOpCode = eSpeculativeLoadPictureSource;
+      mSrcset.Assign(aSrcset);
+      mSizes.Assign(aSizes);
+      mTypeOrCharsetSource.Assign(aType);
+      mMedia.Assign(aMedia);
     }
 
     inline void InitScript(const nsAString& aUrl,
                            const nsAString& aCharset,
                            const nsAString& aType,
                            const nsAString& aCrossOrigin,
                            bool aParserInHead)
     {
@@ -65,19 +107,19 @@ class nsHtml5SpeculativeLoad {
                       "Trying to reinitialize a speculative load!");
       mOpCode = aParserInHead ?
           eSpeculativeLoadScriptFromHead : eSpeculativeLoadScript;
       mUrl.Assign(aUrl);
       mCharset.Assign(aCharset);
       mTypeOrCharsetSource.Assign(aType);
       mCrossOrigin.Assign(aCrossOrigin);
     }
-    
+
     inline void InitStyle(const nsAString& aUrl, const nsAString& aCharset,
-			  const nsAString& aCrossOrigin)
+                          const nsAString& aCrossOrigin)
     {
       NS_PRECONDITION(mOpCode == eSpeculativeLoadUninitialized,
                       "Trying to reinitialize a speculative load!");
       mOpCode = eSpeculativeLoadStyle;
       mUrl.Assign(aUrl);
       mCharset.Assign(aCharset);
       mCrossOrigin.Assign(aCrossOrigin);
     }
@@ -117,17 +159,17 @@ class nsHtml5SpeculativeLoad {
       NS_PRECONDITION(mOpCode == eSpeculativeLoadUninitialized,
                       "Trying to reinitialize a speculative load!");
       mOpCode = eSpeculativeLoadSetDocumentCharset;
       CopyUTF8toUTF16(aCharset, mCharset);
       mTypeOrCharsetSource.Assign((char16_t)aCharsetSource);
     }
 
     void Perform(nsHtml5TreeOpExecutor* aExecutor);
-    
+
   private:
     eHtml5SpeculativeLoad mOpCode;
     nsString mUrl;
     nsString mMetaReferrerPolicy;
     /**
      * If mOpCode is eSpeculativeLoadStyle or eSpeculativeLoadScript[FromHead]
      * then this is the value of the "charset" attribute. For
      * eSpeculativeLoadSetDocumentCharset it is the charset that the
@@ -142,11 +184,27 @@ class nsHtml5SpeculativeLoad {
      */
     nsString mTypeOrCharsetSource;
     /**
      * If mOpCode is eSpeculativeLoadImage or eSpeculativeLoadScript[FromHead],
      * this is the value of the "crossorigin" attribute.  If the
      * attribute is not set, this will be a void string.
      */
     nsString mCrossOrigin;
+    /**
+     * If mOpCode is eSpeculativeLoadImage or eSpeculativeLoadPictureSource,
+     * this is the value of "srcset" attribute.  If the attribute is not set,
+     * this will be a void string.
+     */
+    nsString mSrcset;
+    /**
+     * If mOpCode is eSpeculativeLoadPictureSource, this is the value of "sizes"
+     * attribute.  If the attribute is not set, this will be a void string.
+     */
+    nsString mSizes;
+    /**
+     * If mOpCode is eSpeculativeLoadPictureSource, this is the value of "media"
+     * attribute.  If the attribute is not set, this will be a void string.
+     */
+    nsString mMedia;
 };
 
 #endif // nsHtml5SpeculativeLoad_h
--- a/parser/html/nsHtml5TreeBuilderCppSupplement.h
+++ b/parser/html/nsHtml5TreeBuilderCppSupplement.h
@@ -110,30 +110,53 @@ nsHtml5TreeBuilder::createElement(int32_
   treeOp->Init(aNamespace,
                aName,
                aAttributes,
                content,
                aIntendedParent,
                !!mSpeculativeLoadStage);
   // mSpeculativeLoadStage is non-null only in the off-the-main-thread
   // tree builder, which handles the network stream
-  
+
   // Start wall of code for speculative loading and line numbers
-  
+
   if (mSpeculativeLoadStage) {
     switch (aNamespace) {
       case kNameSpaceID_XHTML:
         if (nsHtml5Atoms::img == aName) {
           nsString* url = aAttributes->getValue(nsHtml5AttributeName::ATTR_SRC);
-          if (url) {
-            nsString* crossOrigin =
-              aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN);
+          nsString* srcset =
+            aAttributes->getValue(nsHtml5AttributeName::ATTR_SRCSET);
+          nsString* crossOrigin =
+            aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN);
+          nsString* sizes =
+            aAttributes->getValue(nsHtml5AttributeName::ATTR_SIZES);
+          mSpeculativeLoadQueue.AppendElement()->
+            InitImage(url ? *url : NullString(),
+                      crossOrigin ? *crossOrigin : NullString(),
+                      srcset ? *srcset : NullString(),
+                      sizes ? *sizes : NullString());
+        } else if (nsHtml5Atoms::source == aName) {
+          nsString* srcset =
+            aAttributes->getValue(nsHtml5AttributeName::ATTR_SRCSET);
+          // Sources without srcset cannot be selected. The source could also be
+          // for a media element, but in that context doesn't use srcset.  See
+          // comments in nsHtml5SpeculativeLoad.h about <picture> preloading
+          if (srcset) {
+            nsString* sizes =
+              aAttributes->getValue(nsHtml5AttributeName::ATTR_SIZES);
+            nsString* type =
+              aAttributes->getValue(nsHtml5AttributeName::ATTR_TYPE);
+            nsString* media =
+              aAttributes->getValue(nsHtml5AttributeName::ATTR_MEDIA);
             mSpeculativeLoadQueue.AppendElement()->
-              InitImage(*url,
-                        crossOrigin ? *crossOrigin : NullString());
+              InitPictureSource(*srcset,
+                                sizes ? *sizes : NullString(),
+                                type ? *type : NullString(),
+                                media ? *media : NullString());
           }
         } else if (nsHtml5Atoms::script == aName) {
           nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
           NS_ASSERTION(treeOp, "Tree op allocation failed.");
           treeOp->Init(eTreeOpSetScriptLineNumberAndFreeze, content, tokenizer->getLineNumber());
 
           nsString* url = aAttributes->getValue(nsHtml5AttributeName::ATTR_SRC);
           if (url) {
@@ -142,17 +165,17 @@ nsHtml5TreeBuilder::createElement(int32_
             nsString* crossOrigin =
               aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN);
             mSpeculativeLoadQueue.AppendElement()->
               InitScript(*url,
                          (charset) ? *charset : EmptyString(),
                          (type) ? *type : EmptyString(),
                          (crossOrigin) ? *crossOrigin : NullString(),
                          mode == NS_HTML5TREE_BUILDER_IN_HEAD);
-            mCurrentHtmlScriptIsAsyncOrDefer = 
+            mCurrentHtmlScriptIsAsyncOrDefer =
               aAttributes->contains(nsHtml5AttributeName::ATTR_ASYNC) ||
               aAttributes->contains(nsHtml5AttributeName::ATTR_DEFER);
           }
         } else if (nsHtml5Atoms::link == aName) {
           nsString* rel = aAttributes->getValue(nsHtml5AttributeName::ATTR_REL);
           // Not splitting on space here is bogus but the old parser didn't even
           // do a case-insensitive check.
           if (rel && rel->LowerCaseEqualsASCII("stylesheet")) {
@@ -165,17 +188,19 @@ nsHtml5TreeBuilder::createElement(int32_
                 InitStyle(*url,
                           (charset) ? *charset : EmptyString(),
                           (crossOrigin) ? *crossOrigin : NullString());
             }
           }
         } else if (nsHtml5Atoms::video == aName) {
           nsString* url = aAttributes->getValue(nsHtml5AttributeName::ATTR_POSTER);
           if (url) {
-            mSpeculativeLoadQueue.AppendElement()->InitImage(*url, NullString());
+            mSpeculativeLoadQueue.AppendElement()->InitImage(*url, NullString(),
+                                                             NullString(),
+                                                             NullString());
           }
         } else if (nsHtml5Atoms::style == aName) {
           nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
           NS_ASSERTION(treeOp, "Tree op allocation failed.");
           treeOp->Init(eTreeOpSetStyleLineNumber, content, tokenizer->getLineNumber());
         } else if (nsHtml5Atoms::html == aName) {
           nsString* url = aAttributes->getValue(nsHtml5AttributeName::ATTR_MANIFEST);
           if (url) {
@@ -199,17 +224,19 @@ nsHtml5TreeBuilder::createElement(int32_
             }
           }
         }
         break;
       case kNameSpaceID_SVG:
         if (nsHtml5Atoms::image == aName) {
           nsString* url = aAttributes->getValue(nsHtml5AttributeName::ATTR_XLINK_HREF);
           if (url) {
-            mSpeculativeLoadQueue.AppendElement()->InitImage(*url, NullString());
+            mSpeculativeLoadQueue.AppendElement()->InitImage(*url, NullString(),
+                                                             NullString(),
+                                                             NullString());
           }
         } else if (nsHtml5Atoms::script == aName) {
           nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
           NS_ASSERTION(treeOp, "Tree op allocation failed.");
           treeOp->Init(eTreeOpSetScriptLineNumberAndFreeze, content, tokenizer->getLineNumber());
 
           nsString* url = aAttributes->getValue(nsHtml5AttributeName::ATTR_XLINK_HREF);
           if (url) {
@@ -231,17 +258,17 @@ nsHtml5TreeBuilder::createElement(int32_
           nsString* url = aAttributes->getValue(nsHtml5AttributeName::ATTR_XLINK_HREF);
           if (url) {
             nsString* crossOrigin =
               aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN);
             mSpeculativeLoadQueue.AppendElement()->
               InitStyle(*url, EmptyString(),
                         (crossOrigin) ? *crossOrigin : NullString());
           }
-        }        
+        }
         break;
     }
   } else if (aNamespace != kNameSpaceID_MathML) {
     // No speculative loader--just line numbers and defer/async check
     if (nsHtml5Atoms::style == aName) {
       nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
       NS_ASSERTION(treeOp, "Tree op allocation failed.");
       treeOp->Init(eTreeOpSetStyleLineNumber, content, tokenizer->getLineNumber());
@@ -735,16 +762,23 @@ nsHtml5TreeBuilder::elementPushed(int32_
       aName == nsHtml5Atoms::menuitem) {
     if (mBuilder) {
       nsHtml5TreeOperation::DoneCreatingElement(static_cast<nsIContent*>(aElement));
     } else {
       mOpQueue.AppendElement()->Init(eTreeOpDoneCreatingElement, aElement);
     }
     return;
   }
+  if (mSpeculativeLoadStage && aName == nsHtml5Atoms::picture) {
+    // mSpeculativeLoadStage is non-null only in the off-the-main-thread
+    // tree builder, which handles the network stream
+    //
+    // See comments in nsHtml5SpeculativeLoad.h about <picture> preloading
+    mSpeculativeLoadQueue.AppendElement()->InitOpenPicture();
+  }
 }
 
 void
 nsHtml5TreeBuilder::elementPopped(int32_t aNamespace, nsIAtom* aName, nsIContentHandle* aElement)
 {
   NS_ASSERTION(aNamespace == kNameSpaceID_XHTML || aNamespace == kNameSpaceID_SVG || aNamespace == kNameSpaceID_MathML, "Element isn't HTML, SVG or MathML!");
   NS_ASSERTION(aName, "Element doesn't have local name!");
   NS_ASSERTION(aElement, "No element!");
@@ -835,16 +869,23 @@ nsHtml5TreeBuilder::elementPopped(int32_
     return;
   }
   if (aName == nsHtml5Atoms::meta && !fragment && !mBuilder) {
     nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
     NS_ASSERTION(treeOp, "Tree op allocation failed.");
     treeOp->Init(eTreeOpProcessMeta, aElement);
     return;
   }
+  if (mSpeculativeLoadStage && aName == nsHtml5Atoms::picture) {
+    // mSpeculativeLoadStage is non-null only in the off-the-main-thread
+    // tree builder, which handles the network stream
+    //
+    // See comments in nsHtml5SpeculativeLoad.h about <picture> preloading
+    mSpeculativeLoadQueue.AppendElement()->InitEndPicture();
+  }
   return;
 }
 
 void
 nsHtml5TreeBuilder::accumulateCharacters(const char16_t* aBuf, int32_t aStart, int32_t aLength)
 {
   int32_t newFillLen = charBufferLen + aLength;
   if (newFillLen > charBuffer.length) {
--- a/parser/html/nsHtml5TreeOpExecutor.cpp
+++ b/parser/html/nsHtml5TreeOpExecutor.cpp
@@ -854,48 +854,66 @@ nsHtml5TreeOpExecutor::IsExternalViewSou
   if (mDocumentURI) {
     mDocumentURI->SchemeIs("view-source", &isViewSource);
   }
   return isViewSource;
 }
 
 // Speculative loading
 
+nsIURI*
+nsHtml5TreeOpExecutor::BaseURIForPreload()
+{
+  // The URL of the document without <base>
+  nsIURI* documentURI = mDocument->GetDocumentURI();
+  // The URL of the document with non-speculative <base>
+  nsIURI* documentBaseURI = mDocument->GetDocBaseURI();
+
+  // If the two above are different, use documentBaseURI. If they are the same,
+  // the document object isn't aware of a <base>, so attempt to use the
+  // mSpeculationBaseURI or, failing, that, documentURI.
+  return (documentURI == documentBaseURI) ?
+          (mSpeculationBaseURI ?
+           mSpeculationBaseURI.get() : documentURI)
+         : documentBaseURI;
+}
+
 already_AddRefed<nsIURI>
 nsHtml5TreeOpExecutor::ConvertIfNotPreloadedYet(const nsAString& aURL)
 {
   if (aURL.IsEmpty()) {
     return nullptr;
   }
-  // The URL of the document without <base>
-  nsIURI* documentURI = mDocument->GetDocumentURI();
-  // The URL of the document with non-speculative <base>
-  nsIURI* documentBaseURI = mDocument->GetDocBaseURI();
 
-  // If the two above are different, use documentBaseURI. If they are the
-  // same, the document object isn't aware of a <base>, so attempt to use the
-  // mSpeculationBaseURI or, failing, that, documentURI.
-  nsIURI* base = (documentURI == documentBaseURI) ?
-                  (mSpeculationBaseURI ?
-                   mSpeculationBaseURI.get() : documentURI)
-                 : documentBaseURI;
+  nsIURI* base = BaseURIForPreload();
   const nsCString& charset = mDocument->GetDocumentCharacterSet();
   nsCOMPtr<nsIURI> uri;
   nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, charset.get(), base);
   if (NS_FAILED(rv)) {
     NS_WARNING("Failed to create a URI");
     return nullptr;
   }
+
+  if (ShouldPreloadURI(uri)) {
+    return uri.forget();
+  }
+
+  return nullptr;
+}
+
+bool
+nsHtml5TreeOpExecutor::ShouldPreloadURI(nsIURI *aURI)
+{
   nsAutoCString spec;
-  uri->GetSpec(spec);
+  aURI->GetSpec(spec);
   if (mPreloadedURLs.Contains(spec)) {
-    return nullptr;
+    return false;
   }
   mPreloadedURLs.PutEntry(spec);
-  return uri.forget();
+  return true;
 }
 
 void
 nsHtml5TreeOpExecutor::PreloadScript(const nsAString& aURL,
                                      const nsAString& aCharset,
                                      const nsAString& aType,
                                      const nsAString& aCrossOrigin,
                                      bool aScriptFromHead)
@@ -919,23 +937,49 @@ nsHtml5TreeOpExecutor::PreloadStyle(cons
     return;
   }
   mDocument->PreloadStyle(uri, aCharset, aCrossOrigin,
                           mSpeculationReferrerPolicy);
 }
 
 void
 nsHtml5TreeOpExecutor::PreloadImage(const nsAString& aURL,
-                                    const nsAString& aCrossOrigin)
+                                    const nsAString& aCrossOrigin,
+                                    const nsAString& aSrcset,
+                                    const nsAString& aSizes)
 {
-  nsCOMPtr<nsIURI> uri = ConvertIfNotPreloadedYet(aURL);
-  if (!uri) {
-    return;
+  nsCOMPtr<nsIURI> baseURI = BaseURIForPreload();
+  nsCOMPtr<nsIURI> uri = mDocument->ResolvePreloadImage(baseURI, aURL, aSrcset,
+                                                        aSizes);
+  if (uri && ShouldPreloadURI(uri)) {
+    mDocument->MaybePreLoadImage(uri, aCrossOrigin, mSpeculationReferrerPolicy);
   }
-  mDocument->MaybePreLoadImage(uri, aCrossOrigin, mSpeculationReferrerPolicy);
+}
+
+// These calls inform the document of picture state and seen sources, such that
+// it can use them to inform ResolvePreLoadImage as necessary
+void
+nsHtml5TreeOpExecutor::PreloadPictureSource(const nsAString& aSrcset,
+                                            const nsAString& aSizes,
+                                            const nsAString& aType,
+                                            const nsAString& aMedia)
+{
+  mDocument->PreloadPictureImageSource(aSrcset, aSizes, aType, aMedia);
+}
+
+void
+nsHtml5TreeOpExecutor::PreloadOpenPicture()
+{
+  mDocument->PreloadPictureOpened();
+}
+
+void
+nsHtml5TreeOpExecutor::PreloadEndPicture()
+{
+  mDocument->PreloadPictureClosed();
 }
 
 void
 nsHtml5TreeOpExecutor::AddBase(const nsAString& aURL)
 {
   const nsCString& charset = mDocument->GetDocumentCharacterSet();
   nsresult rv = NS_NewURI(getter_AddRefs(mViewSourceBaseURI), aURL,
                                      charset.get(), GetViewSourceBaseURI());
--- a/parser/html/nsHtml5TreeOpExecutor.h
+++ b/parser/html/nsHtml5TreeOpExecutor.h
@@ -249,34 +249,56 @@ class nsHtml5TreeOpExecutor MOZ_FINAL : 
 
     void PreloadScript(const nsAString& aURL,
                        const nsAString& aCharset,
                        const nsAString& aType,
                        const nsAString& aCrossOrigin,
                        bool aScriptFromHead);
 
     void PreloadStyle(const nsAString& aURL, const nsAString& aCharset,
-		      const nsAString& aCrossOrigin);
+                      const nsAString& aCrossOrigin);
+
+    void PreloadImage(const nsAString& aURL,
+                      const nsAString& aCrossOrigin,
+                      const nsAString& aSrcset,
+                      const nsAString& aSizes);
 
-    void PreloadImage(const nsAString& aURL, const nsAString& aCrossOrigin);
+    void PreloadOpenPicture();
+
+    void PreloadEndPicture();
+
+    void PreloadPictureSource(const nsAString& aSrcset,
+                              const nsAString& aSizes,
+                              const nsAString& aType,
+                              const nsAString& aMedia);
 
     void SetSpeculationBase(const nsAString& aURL);
 
     void SetSpeculationReferrerPolicy(ReferrerPolicy aReferrerPolicy);
     void SetSpeculationReferrerPolicy(const nsAString& aReferrerPolicy);
-    
+
     void AddBase(const nsAString& aURL);
 
     static void InitializeStatics();
 
   private:
     nsHtml5Parser* GetParser();
 
     bool IsExternalViewSource();
 
     /**
      * Get a nsIURI for an nsString if the URL hasn't been preloaded yet.
      */
     already_AddRefed<nsIURI> ConvertIfNotPreloadedYet(const nsAString& aURL);
 
+    /**
+     * The base URI we would use for current preload operations
+     */
+    nsIURI* BaseURIForPreload();
+
+    /**
+     * Returns true if we haven't preloaded this URI yet, and adds it to the
+     * list of preloaded URIs
+     */
+    bool ShouldPreloadURI(nsIURI *aURI);
 };
 
 #endif // nsHtml5TreeOpExecutor_h