Bug 1243846 - Implement Intersection Observer API. r=mrbkap, r=mstange
☠☠ backed out by 1e7d830ec828 ☠ ☠
authorTobias Schneider <schneider@jancona.com>
Wed, 26 Oct 2016 22:04:00 -0400
changeset 319843 111c1227f51eb6e94d7213f92b2a32e55bfdf3e8
parent 319842 76d7f4eb6100ffc26f0142688d3c797ee8fefa6e
child 319844 68f683a2034bb280a323242b4de87c958ed7c6f4
push id20749
push userryanvm@gmail.com
push dateSat, 29 Oct 2016 13:21:21 +0000
treeherderfx-team@1b170b39ed6b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmrbkap, mstange
bugs1243846
milestone52.0a1
Bug 1243846 - Implement Intersection Observer API. r=mrbkap, r=mstange
dom/base/DOMIntersectionObserver.cpp
dom/base/DOMIntersectionObserver.h
dom/base/Element.cpp
dom/base/Element.h
dom/base/FragmentOrElement.h
dom/base/moz.build
dom/base/nsDocument.cpp
dom/base/nsDocument.h
dom/base/nsIDocument.h
dom/base/test/intersectionobserver_iframe.html
dom/base/test/intersectionobserver_window.html
dom/base/test/mochitest.ini
dom/base/test/test_intersectionobservers.html
dom/bindings/Bindings.conf
dom/bindings/Errors.msg
dom/tests/mochitest/general/test_interfaces.html
dom/webidl/DOMRect.webidl
dom/webidl/IntersectionObserver.webidl
dom/webidl/moz.build
layout/base/nsRefreshDriver.cpp
layout/base/nsRefreshDriver.h
layout/style/nsCSSParser.cpp
layout/style/nsCSSParser.h
layout/style/nsCSSPropertyID.h
layout/style/nsCSSValue.cpp
layout/style/nsCSSValue.h
new file mode 100644
--- /dev/null
+++ b/dom/base/DOMIntersectionObserver.cpp
@@ -0,0 +1,466 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DOMIntersectionObserver.h"
+#include "nsCSSParser.h"
+#include "nsCSSPropertyID.h"
+#include "nsIFrame.h"
+#include "nsContentUtils.h"
+#include "nsLayoutUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserverEntry)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserverEntry)
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMIntersectionObserverEntry, mOwner,
+                                      mRootBounds, mBoundingClientRect,
+                                      mIntersectionRect, mTarget)
+
+double
+DOMIntersectionObserverEntry::IntersectionRatio()
+{
+  double targetArea = mBoundingClientRect->Width() * mBoundingClientRect->Height();
+  double intersectionArea = mIntersectionRect->Width() * mIntersectionRect->Height();
+  double intersectionRatio = targetArea > 0.0 ? intersectionArea / targetArea : 0.0;
+  return intersectionRatio;
+}
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+  NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserver)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserver)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(DOMIntersectionObserver)
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+already_AddRefed<DOMIntersectionObserver>
+DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
+                                     mozilla::dom::IntersectionCallback& aCb,
+                                     mozilla::ErrorResult& aRv)
+{
+  return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv);
+}
+
+already_AddRefed<DOMIntersectionObserver>
+DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
+                                     mozilla::dom::IntersectionCallback& aCb,
+                                     const mozilla::dom::IntersectionObserverInit& aOptions,
+                                     mozilla::ErrorResult& aRv)
+{
+  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports());
+  if (!window) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+  RefPtr<DOMIntersectionObserver> observer =
+    new DOMIntersectionObserver(window.forget(), aCb);
+
+  observer->mRoot = aOptions.mRoot;
+
+  if (!observer->SetRootMargin(aOptions.mRootMargin)) {
+    aRv.ThrowDOMException(NS_ERROR_DOM_SYNTAX_ERR,
+      NS_LITERAL_CSTRING("rootMargin must be specified in pixels or percent."));
+    return nullptr;
+  }
+
+  if (aOptions.mThreshold.IsDoubleSequence()) {
+    const mozilla::dom::Sequence<double>& thresholds = aOptions.mThreshold.GetAsDoubleSequence();
+    observer->mThresholds.SetCapacity(thresholds.Length());
+    for (const auto& thresh : thresholds) {
+      if (thresh < 0.0 || thresh > 1.0) {
+        aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
+        return nullptr;
+      }
+      observer->mThresholds.AppendElement(thresh);
+    }
+    observer->mThresholds.Sort();
+  } else {
+    double thresh = aOptions.mThreshold.GetAsDouble();
+    if (thresh < 0.0 || thresh > 1.0) {
+      aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
+      return nullptr;
+    }
+    observer->mThresholds.AppendElement(thresh);
+  }
+
+  return observer.forget();
+}
+
+bool
+DOMIntersectionObserver::SetRootMargin(const nsAString& aString)
+{
+  // By not passing a CSS Loader object we make sure we don't parse in quirks
+  // mode so that pixel/percent and unit-less values will be differentiated.
+  nsCSSParser parser(nullptr);
+  nsCSSValue value;
+  if (!parser.ParseMarginString(aString, nullptr, 0, value, true)) {
+    return false;
+  }
+
+  mRootMargin = value.GetRectValue();
+
+  for (uint32_t i = 0; i < ArrayLength(nsCSSRect::sides); ++i) {
+    nsCSSValue value = mRootMargin.*nsCSSRect::sides[i];
+    if (!(value.IsPixelLengthUnit() || value.IsPercentLengthUnit())) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+void
+DOMIntersectionObserver::GetRootMargin(mozilla::dom::DOMString& aRetVal)
+{
+  mRootMargin.AppendToString(eCSSProperty_DOM, aRetVal, nsCSSValue::eNormalized);
+}
+
+void
+DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal)
+{
+  aRetVal = mThresholds;
+}
+
+void
+DOMIntersectionObserver::Observe(Element& aTarget)
+{
+  if (mObservationTargets.Contains(&aTarget)) {
+    return;
+  }
+  aTarget.RegisterIntersectionObserver(this);
+  mObservationTargets.PutEntry(&aTarget);
+  Connect();
+}
+
+void
+DOMIntersectionObserver::Unobserve(Element& aTarget)
+{
+  if (!mObservationTargets.Contains(&aTarget)) {
+    return;
+  }
+  if (mObservationTargets.Count() == 1) {
+    Disconnect();
+    return;
+  }
+  aTarget.UnregisterIntersectionObserver(this);
+  mObservationTargets.RemoveEntry(&aTarget);
+}
+
+void
+DOMIntersectionObserver::Connect()
+{
+  if (mConnected) {
+    return;
+  }
+  nsIDocument* document = mOwner->GetExtantDoc();
+  document->AddIntersectionObserver(this);
+  mConnected = true;
+}
+
+void
+DOMIntersectionObserver::Disconnect()
+{
+  if (!mConnected) {
+    return;
+  }
+  for (auto iter = mObservationTargets.Iter(); !iter.Done(); iter.Next()) {
+    Element* target = iter.Get()->GetKey();
+    target->UnregisterIntersectionObserver(this);
+  }
+  mObservationTargets.Clear();
+  nsIDocument* document = mOwner->GetExtantDoc();
+  document->RemoveIntersectionObserver(this);
+  mConnected = false;
+}
+
+void
+DOMIntersectionObserver::TakeRecords(nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal)
+{
+  aRetVal.SwapElements(mQueuedEntries);
+  mQueuedEntries.Clear();
+}
+
+static bool
+CheckSimilarOrigin(nsINode* aNode1, nsINode* aNode2)
+{
+  nsIPrincipal* principal1 = aNode1->NodePrincipal();
+  nsIPrincipal* principal2 = aNode2->NodePrincipal();
+  nsAutoCString baseDomain1;
+  nsAutoCString baseDomain2;
+
+  nsresult rv = principal1->GetBaseDomain(baseDomain1);
+  if (NS_FAILED(rv)) {
+    return principal1 == principal2;
+  }
+
+  rv = principal2->GetBaseDomain(baseDomain2);
+  if (NS_FAILED(rv)) {
+    return principal1 == principal2;
+  }
+
+  return baseDomain1 == baseDomain2;
+}
+
+static Maybe<nsRect>
+EdgeInclusiveIntersection(const nsRect& aRect, const nsRect& aOtherRect)
+{
+  nscoord left = std::max(aRect.x, aOtherRect.x);
+  nscoord top = std::max(aRect.y, aOtherRect.y);
+  nscoord right = std::min(aRect.XMost(), aOtherRect.XMost());
+  nscoord bottom = std::min(aRect.YMost(), aOtherRect.YMost());
+  if (left > right || top > bottom) {
+    return Nothing();
+  }
+  return Some(nsRect(left, top, right - left, bottom - top));
+}
+
+void
+DOMIntersectionObserver::Update(nsIDocument* aDocument, DOMHighResTimeStamp time)
+{
+  Element* root;
+  nsIFrame* rootFrame;
+  nsRect rootRect;
+
+  if (mRoot) {
+    root = mRoot;
+    rootFrame = root->GetPrimaryFrame();
+    if (rootFrame) {
+      if (rootFrame->GetType() == nsGkAtoms::scrollFrame) {
+        nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
+        rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
+          rootFrame,
+          rootFrame->GetContentRectRelativeToSelf(),
+          scrollFrame->GetScrolledFrame());
+      } else {
+        rootRect = nsLayoutUtils::GetAllInFlowRectsUnion(rootFrame,
+          nsLayoutUtils::GetContainingBlockForClientRect(rootFrame),
+          nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
+      }
+    }
+  } else {
+    nsCOMPtr<nsIPresShell> presShell = aDocument->GetShell();
+    if (presShell) {
+      rootFrame = presShell->GetRootScrollFrame();
+      nsPresContext* presContext = rootFrame->PresContext();
+      while (!presContext->IsRootContentDocument()) {
+        presContext = rootFrame->PresContext()->GetParentPresContext();
+        rootFrame = presContext->PresShell()->GetRootScrollFrame();
+      }
+      root = rootFrame->GetContent()->AsElement();
+      nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
+      rootRect = scrollFrame->GetScrollPortRect();
+    }
+  }
+
+  nsMargin rootMargin;
+  NS_FOR_CSS_SIDES(side) {
+    nscoord basis = side == NS_SIDE_TOP || side == NS_SIDE_BOTTOM ?
+      rootRect.height : rootRect.width;
+    nsCSSValue value = mRootMargin.*nsCSSRect::sides[side];
+    nsStyleCoord coord;
+    if (value.IsPixelLengthUnit()) {
+      coord.SetCoordValue(value.GetPixelLength());
+    } else if (value.IsPercentLengthUnit()) {
+      coord.SetPercentValue(value.GetPercentValue());
+    } else {
+      MOZ_ASSERT_UNREACHABLE("invalid length unit");
+    }
+    rootMargin.Side(side) = nsLayoutUtils::ComputeCBDependentValue(basis, coord);
+  }
+
+  for (auto iter = mObservationTargets.Iter(); !iter.Done(); iter.Next()) {
+    Element* target = iter.Get()->GetKey();
+    nsIFrame* targetFrame = target->GetPrimaryFrame();
+    nsRect targetRect;
+    Maybe<nsRect> intersectionRect;
+
+    if (rootFrame && targetFrame) {
+      // If mRoot is set we are testing intersection with a container element
+      // instead of the implicit root.
+      if (mRoot) {
+        // Skip further processing of this target if it is not in the same
+        // Document as the intersection root, e.g. if root is an element of
+        // the main document and target an element from an embedded iframe.
+        if (target->GetComposedDoc() != root->GetComposedDoc()) {
+          continue;
+        }
+        // Skip further processing of this target if is not a descendant of the
+        // intersection root in the containing block chain. E.g. this would be
+        // the case if the target is in a position:absolute element whose
+        // containing block is an ancestor of root.
+        if (!nsLayoutUtils::IsAncestorFrameCrossDoc(rootFrame, targetFrame)) {
+          continue;
+        }
+      }
+
+      targetRect = nsLayoutUtils::GetAllInFlowRectsUnion(
+        targetFrame,
+        nsLayoutUtils::GetContainingBlockForClientRect(targetFrame),
+        nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS
+      );
+      intersectionRect = Some(targetFrame->GetVisualOverflowRect());
+
+      nsIFrame* containerFrame = nsLayoutUtils::GetCrossDocParentFrame(targetFrame);
+      while (containerFrame && containerFrame != rootFrame) {
+        if (containerFrame->GetType() == nsGkAtoms::scrollFrame) {
+          nsIScrollableFrame* scrollFrame = do_QueryFrame(containerFrame);
+          nsRect subFrameRect = scrollFrame->GetScrollPortRect();
+          nsRect intersectionRectRelativeToContainer =
+            nsLayoutUtils::TransformFrameRectToAncestor(targetFrame,
+                                                        intersectionRect.value(),
+                                                        containerFrame);
+          intersectionRect = EdgeInclusiveIntersection(intersectionRectRelativeToContainer,
+                                                       subFrameRect);
+          if (!intersectionRect) {
+            break;
+          }
+          targetFrame = containerFrame;
+        }
+
+        // TODO: Apply clip-path.
+
+        containerFrame = nsLayoutUtils::GetCrossDocParentFrame(containerFrame);
+      }
+    }
+
+    nsRect rootIntersectionRect = rootRect;
+    bool isInSimilarOriginBrowsingContext = CheckSimilarOrigin(root, target);
+
+    if (isInSimilarOriginBrowsingContext) {
+      rootIntersectionRect.Inflate(rootMargin);
+    }
+
+    if (intersectionRect.isSome()) {
+      nsRect intersectionRectRelativeToRoot =
+        nsLayoutUtils::TransformFrameRectToAncestor(
+          targetFrame,
+          intersectionRect.value(),
+          nsLayoutUtils::GetContainingBlockForClientRect(rootFrame)
+      );
+      intersectionRect = EdgeInclusiveIntersection(
+        intersectionRectRelativeToRoot,
+        rootIntersectionRect
+      );
+      if (intersectionRect.isSome()) {
+        intersectionRect = Some(nsLayoutUtils::TransformFrameRectToAncestor(
+          nsLayoutUtils::GetContainingBlockForClientRect(rootFrame),
+          intersectionRect.value(),
+          targetFrame->PresContext()->PresShell()->GetRootScrollFrame()
+        ));
+      }
+    }
+
+    double targetArea = targetRect.width * targetRect.height;
+    double intersectionArea = !intersectionRect ?
+      0 : intersectionRect->width * intersectionRect->height;
+    double intersectionRatio = targetArea > 0.0 ? intersectionArea / targetArea : 0.0;
+
+    size_t threshold = -1;
+    if (intersectionRatio > 0.0) {
+      if (intersectionRatio >= 1.0) {
+        intersectionRatio = 1.0;
+        threshold = mThresholds.Length();
+      } else {
+        for (size_t k = 0; k < mThresholds.Length(); ++k) {
+          if (mThresholds[k] <= intersectionRatio) {
+            threshold = k + 1;
+          } else {
+            break;
+          }
+        }
+      }
+    } else if (intersectionRect.isSome()) {
+      threshold = 0;
+    }
+
+    if (target->UpdateIntersectionObservation(this, threshold)) {
+      QueueIntersectionObserverEntry(
+        target, time,
+        isInSimilarOriginBrowsingContext ? Some(rootIntersectionRect) : Nothing(),
+        targetRect, intersectionRect
+      );
+    }
+  }
+}
+
+void
+DOMIntersectionObserver::QueueIntersectionObserverEntry(Element* aTarget,
+                                                        DOMHighResTimeStamp time,
+                                                        const Maybe<nsRect>& aRootRect,
+                                                        const nsRect& aTargetRect,
+                                                        const Maybe<nsRect>& aIntersectionRect)
+{
+  RefPtr<DOMRect> rootBounds;
+  if (aRootRect.isSome()) {
+    rootBounds = new DOMRect(this);
+    rootBounds->SetLayoutRect(aRootRect.value());
+  }
+  RefPtr<DOMRect> boundingClientRect = new DOMRect(this);
+  boundingClientRect->SetLayoutRect(aTargetRect);
+  RefPtr<DOMRect> intersectionRect = new DOMRect(this);
+  if (aIntersectionRect.isSome()) {
+    intersectionRect->SetLayoutRect(aIntersectionRect.value());
+  }
+  RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
+    this,
+    time,
+    rootBounds.forget(),
+    boundingClientRect.forget(),
+    intersectionRect.forget(),
+    aTarget);
+  mQueuedEntries.AppendElement(entry.forget());
+}
+
+void
+DOMIntersectionObserver::Notify()
+{
+  if (!mQueuedEntries.Length()) {
+    return;
+  }
+  mozilla::dom::Sequence<mozilla::OwningNonNull<DOMIntersectionObserverEntry>> entries;
+  if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) {
+    for (uint32_t i = 0; i < mQueuedEntries.Length(); ++i) {
+      RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i];
+      *entries.AppendElement(mozilla::fallible) = next;
+    }
+  }
+  mQueuedEntries.Clear();
+  mCallback->Call(this, entries, *this);
+}
+
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/DOMIntersectionObserver.h
@@ -0,0 +1,172 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOMIntersectionObserver_h
+#define DOMIntersectionObserver_h
+
+#include "mozilla/dom/IntersectionObserverBinding.h"
+#include "nsCSSValue.h"
+#include "nsTArray.h"
+
+using mozilla::dom::DOMRect;
+using mozilla::dom::Element;
+
+namespace mozilla {
+namespace dom {
+
+class DOMIntersectionObserver;
+
+class DOMIntersectionObserverEntry final : public nsISupports,
+                                           public nsWrapperCache
+{
+  ~DOMIntersectionObserverEntry() {}
+
+public:
+  DOMIntersectionObserverEntry(nsISupports* aOwner,
+                               DOMHighResTimeStamp aTime,
+                               RefPtr<DOMRect> aRootBounds,
+                               RefPtr<DOMRect> aBoundingClientRect,
+                               RefPtr<DOMRect> aIntersectionRect,
+                               Element* aTarget)
+  : mOwner(aOwner),
+    mTime(aTime),
+    mRootBounds(aRootBounds),
+    mBoundingClientRect(aBoundingClientRect),
+    mIntersectionRect(aIntersectionRect),
+    mTarget(aTarget)
+  {
+  }
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DOMIntersectionObserverEntry)
+
+  nsISupports* GetParentObject() const
+  {
+    return mOwner;
+  }
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+  {
+    return mozilla::dom::IntersectionObserverEntryBinding::Wrap(aCx, this, aGivenProto);
+  }
+
+  DOMHighResTimeStamp Time()
+  {
+    return mTime;
+  }
+
+  DOMRect* GetRootBounds()
+  {
+    return mRootBounds;
+  }
+
+  DOMRect* BoundingClientRect()
+  {
+    return mBoundingClientRect;
+  }
+
+  DOMRect* IntersectionRect()
+  {
+    return mIntersectionRect;
+  }
+
+  double IntersectionRatio();
+
+  Element* Target()
+  {
+    return mTarget;
+  }
+
+protected:
+  nsCOMPtr<nsISupports> mOwner;
+  DOMHighResTimeStamp   mTime;
+  RefPtr<DOMRect>       mRootBounds;
+  RefPtr<DOMRect>       mBoundingClientRect;
+  RefPtr<DOMRect>       mIntersectionRect;
+  RefPtr<Element>       mTarget;
+};
+
+#define NS_DOM_INTERSECTION_OBSERVER_IID \
+{ 0x8570a575, 0xe303, 0x4d18, \
+  { 0xb6, 0xb1, 0x4d, 0x2b, 0x49, 0xd8, 0xef, 0x94 } }
+
+class DOMIntersectionObserver final : public nsISupports,
+                                      public nsWrapperCache
+{
+  virtual ~DOMIntersectionObserver() { }
+
+public:
+  DOMIntersectionObserver(already_AddRefed<nsPIDOMWindowInner>&& aOwner,
+                          mozilla::dom::IntersectionCallback& aCb)
+  : mOwner(aOwner), mCallback(&aCb), mConnected(false)
+  {
+  }
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DOMIntersectionObserver)
+  NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOM_INTERSECTION_OBSERVER_IID)
+
+  static already_AddRefed<DOMIntersectionObserver>
+  Constructor(const mozilla::dom::GlobalObject& aGlobal,
+              mozilla::dom::IntersectionCallback& aCb,
+              mozilla::ErrorResult& aRv);
+  static already_AddRefed<DOMIntersectionObserver>
+  Constructor(const mozilla::dom::GlobalObject& aGlobal,
+              mozilla::dom::IntersectionCallback& aCb,
+              const mozilla::dom::IntersectionObserverInit& aOptions,
+              mozilla::ErrorResult& aRv);
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+  {
+    return mozilla::dom::IntersectionObserverBinding::Wrap(aCx, this, aGivenProto);
+  }
+
+  nsISupports* GetParentObject() const
+  {
+    return mOwner;
+  }
+
+  Element* GetRoot() const {
+    return mRoot;
+  }
+
+  void GetRootMargin(mozilla::dom::DOMString& aRetVal);
+  void GetThresholds(nsTArray<double>& aRetVal);
+  void Observe(Element& aTarget);
+  void Unobserve(Element& aTarget);
+
+  void Disconnect();
+  void TakeRecords(nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal);
+
+  mozilla::dom::IntersectionCallback* IntersectionCallback() { return mCallback; }
+
+  bool SetRootMargin(const nsAString& aString);
+
+  void Update(nsIDocument* aDocument, DOMHighResTimeStamp time);
+  void Notify();
+
+protected:
+  void Connect();
+  void QueueIntersectionObserverEntry(Element* aTarget,
+                                      DOMHighResTimeStamp time,
+                                      const Maybe<nsRect>& aRootRect,
+                                      const nsRect& aTargetRect,
+                                      const Maybe<nsRect>& aIntersectionRect);
+
+  nsCOMPtr<nsPIDOMWindowInner>                    mOwner;
+  RefPtr<mozilla::dom::IntersectionCallback>      mCallback;
+  RefPtr<Element>                                 mRoot;
+  nsCSSRect                                       mRootMargin;
+  nsTArray<double>                                mThresholds;
+  nsTHashtable<nsPtrHashKey<Element>>             mObservationTargets;
+  nsTArray<RefPtr<DOMIntersectionObserverEntry>>  mQueuedEntries;
+  bool                                            mConnected;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(DOMIntersectionObserver, NS_DOM_INTERSECTION_OBSERVER_IID)
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -144,16 +144,17 @@
 #include "mozilla/dom/KeyframeEffectBinding.h"
 #include "mozilla/dom/WindowBinding.h"
 #include "mozilla/dom/ElementBinding.h"
 #include "mozilla/dom/VRDisplay.h"
 #include "mozilla/IntegerPrintfMacros.h"
 #include "mozilla/Preferences.h"
 #include "nsComputedDOMStyle.h"
 #include "nsDOMStringMap.h"
+#include "DOMIntersectionObserver.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 nsIAtom*
 nsIContent::DoGetID() const
 {
   MOZ_ASSERT(HasID(), "Unexpected call");
@@ -3881,8 +3882,49 @@ Element::ClearDataset()
 {
   nsDOMSlots *slots = GetExistingDOMSlots();
 
   MOZ_ASSERT(slots && slots->mDataset,
              "Slots should exist and dataset should not be null.");
   slots->mDataset = nullptr;
 }
 
+nsTArray<Element::nsDOMSlots::IntersectionObserverRegistration>*
+Element::RegisteredIntersectionObservers()
+{
+  nsDOMSlots* slots = DOMSlots();
+  return &slots->mRegisteredIntersectionObservers;
+}
+
+void
+Element::RegisterIntersectionObserver(DOMIntersectionObserver* aObserver)
+{
+  RegisteredIntersectionObservers()->AppendElement(
+    nsDOMSlots::IntersectionObserverRegistration { aObserver, -1 });
+}
+
+void
+Element::UnregisterIntersectionObserver(DOMIntersectionObserver* aObserver)
+{
+  nsTArray<nsDOMSlots::IntersectionObserverRegistration>* observers =
+    RegisteredIntersectionObservers();
+  for (uint32_t i = 0; i < observers->Length(); ++i) {
+    nsDOMSlots::IntersectionObserverRegistration reg = observers->ElementAt(i);
+    if (reg.observer == aObserver) {
+      observers->RemoveElementAt(i);
+      break;
+    }
+  }
+}
+
+bool
+Element::UpdateIntersectionObservation(DOMIntersectionObserver* aObserver, int32_t aThreshold)
+{
+  nsTArray<nsDOMSlots::IntersectionObserverRegistration>* observers =
+    RegisteredIntersectionObservers();
+  for (auto& reg : *observers) {
+    if (reg.observer == aObserver && reg.previousThreshold != aThreshold) {
+      reg.previousThreshold = aThreshold;
+      return true;
+    }
+  }
+  return false;
+}
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -34,16 +34,17 @@
 #include "nsAttrValue.h"
 #include "mozilla/EventForwards.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/DOMTokenListSupportedTokens.h"
 #include "mozilla/dom/WindowBinding.h"
 #include "mozilla/dom/ElementBinding.h"
 #include "mozilla/dom/Nullable.h"
 #include "Units.h"
+#include "DOMIntersectionObserver.h"
 
 class nsIFrame;
 class nsIDOMMozNamedAttrMap;
 class nsIURI;
 class nsIScrollableFrame;
 class nsAttrValueOrString;
 class nsContentList;
 class nsDOMTokenList;
@@ -56,16 +57,17 @@ class nsDocument;
 class nsDOMStringMap;
 
 namespace mozilla {
 class DeclarationBlock;
 namespace dom {
   struct AnimationFilter;
   struct ScrollIntoViewOptions;
   struct ScrollToOptions;
+  class DOMIntersectionObserver;
   class ElementOrCSSPseudoElement;
   class UnrestrictedDoubleOrKeyframeAnimationOptions;
 } // namespace dom
 } // namespace mozilla
 
 
 already_AddRefed<nsContentList>
 NS_GetContentList(nsINode* aRootNode,
@@ -1144,16 +1146,20 @@ public:
    * sorts of elements expose it to JS as a .dataset property
    */
   // Getter, to be called from bindings.
   already_AddRefed<nsDOMStringMap> Dataset();
   // Callback for destructor of dataset to ensure to null out our weak pointer
   // to it.
   void ClearDataset();
 
+  void RegisterIntersectionObserver(DOMIntersectionObserver* aObserver);
+  void UnregisterIntersectionObserver(DOMIntersectionObserver* aObserver);
+  bool UpdateIntersectionObservation(DOMIntersectionObserver* aObserver, int32_t threshold);
+
 protected:
   /*
    * Named-bools for use with SetAttrAndNotify to make call sites easier to
    * read.
    */
   static const bool kFireMutationEvent           = true;
   static const bool kDontFireMutationEvent       = false;
   static const bool kNotifyDocumentObservers     = true;
@@ -1357,16 +1363,18 @@ protected:
    * the value of xlink:show, converted to a suitably equivalent named target
    * (e.g. _blank).
    */
   virtual void GetLinkTarget(nsAString& aTarget);
 
   nsDOMTokenList* GetTokenList(nsIAtom* aAtom,
                                const DOMTokenListSupportedTokenArray aSupportedTokens = nullptr);
 
+  nsTArray<nsDOMSlots::IntersectionObserverRegistration>* RegisteredIntersectionObservers();
+
 private:
   /**
    * Get this element's client area rect in app units.
    * @return the frame's client area
    */
   nsRect GetClientAreaRect();
 
   nsIScrollableFrame* GetScrollFrame(nsIFrame **aStyledFrame = nullptr,
--- a/dom/base/FragmentOrElement.h
+++ b/dom/base/FragmentOrElement.h
@@ -32,16 +32,17 @@ class nsIDocument;
 class nsDOMStringMap;
 class nsIURI;
 
 namespace mozilla {
 namespace css {
 class Declaration;
 } // namespace css
 namespace dom {
+class DOMIntersectionObserver;
 class Element;
 } // namespace dom
 } // namespace mozilla
 
 /**
  * A class that implements nsIWeakReference
  */
 
@@ -339,16 +340,26 @@ public:
      * XBL binding installed on the lement.
      */
     nsCOMPtr<nsIContent> mXBLInsertionParent;
 
     /**
      * Web components custom element data.
      */
     RefPtr<CustomElementData> mCustomElementData;
+
+    /**
+     * Registered Intersection Observers on the element.
+     */
+    struct IntersectionObserverRegistration {
+      DOMIntersectionObserver* observer;
+      int32_t previousThreshold;
+    };
+
+    nsTArray<IntersectionObserverRegistration> mRegisteredIntersectionObservers;
   };
 
 protected:
   void GetMarkup(bool aIncludeSelf, nsAString& aMarkup);
   void SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult& aError);
 
   // Override from nsINode
   virtual nsINode::nsSlots* CreateSlots() override;
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -160,16 +160,17 @@ EXPORTS.mozilla.dom += [
     'CustomElementRegistry.h',
     'DirectionalityUtils.h',
     'DocumentFragment.h',
     'DocumentType.h',
     'DOMCursor.h',
     'DOMError.h',
     'DOMException.h',
     'DOMImplementation.h',
+    'DOMIntersectionObserver.h',
     'DOMMatrix.h',
     'DOMParser.h',
     'DOMPoint.h',
     'DOMQuad.h',
     'DOMRect.h',
     'DOMRequest.h',
     'DOMStringList.h',
     'DOMTokenListSupportedTokens.h',
@@ -353,16 +354,18 @@ UNIFIED_SOURCES += [
 
 if CONFIG['MOZ_WEBRTC']:
     UNIFIED_SOURCES += [
         'nsDOMDataChannel.cpp',
     ]
 
 # these files couldn't be in UNIFIED_SOURCES for now for reasons given below:
 SOURCES += [
+    # Several conflicts with other bindings.
+    'DOMIntersectionObserver.cpp',
     # Because of OS X headers.
     'nsContentUtils.cpp',
     # this file doesn't like windows.h
     'nsDOMWindowUtils.cpp',
     # Conflicts with windows.h's definition of SendMessage.
     'nsFrameMessageManager.cpp',
     # This file has a #error "Never include windows.h in this file!"
     'nsGlobalWindow.cpp',
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -1446,16 +1446,18 @@ nsDocument::~nsDocument()
   mInDestructor = true;
   mInUnlinkOrDeletion = true;
 
   mozilla::DropJSObjects(this);
 
   // Clear mObservers to keep it in sync with the mutationobserver list
   mObservers.Clear();
 
+  mIntersectionObservers.Clear();
+
   if (mStyleSheetSetList) {
     mStyleSheetSetList->Disconnect();
   }
 
   if (mAnimationController) {
     mAnimationController->Disconnect();
   }
 
@@ -1715,16 +1717,18 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildrenCollection)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnonymousContents)
 
   // Traverse all our nsCOMArrays.
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyleSheets)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOnDemandBuiltInUASheets)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPreloadingImages)
 
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIntersectionObservers)
+
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSubImportLinks)
 
   for (uint32_t i = 0; i < tmp->mFrameRequestCallbacks.Length(); ++i) {
     NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mFrameRequestCallbacks[i]");
     cb.NoteXPCOMChild(tmp->mFrameRequestCallbacks[i].mCallback);
   }
 
   // Traverse animation components
@@ -1801,16 +1805,18 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mImportManager)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSubImportLinks)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mFontFaceSet)
 
   tmp->mParentDocument = nullptr;
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPreloadingImages)
 
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mIntersectionObservers)
+
   tmp->ClearAllBoxObjects();
 
   if (tmp->mListenerManager) {
     tmp->mListenerManager->Disconnect();
     tmp->UnsetFlags(NODE_HAS_LISTENERMANAGER);
     tmp->mListenerManager = nullptr;
   }
 
@@ -12306,16 +12312,61 @@ nsDocument::ReportUseCounters()
 
           Telemetry::Accumulate(id, 1);
         }
       }
     }
   }
 }
 
+void
+nsDocument::AddIntersectionObserver(DOMIntersectionObserver* aObserver)
+{
+  NS_ASSERTION(mIntersectionObservers.IndexOf(aObserver) == nsTArray<int>::NoIndex,
+               "Intersection observer already in the list");
+  mIntersectionObservers.AppendElement(aObserver);
+}
+
+void
+nsDocument::RemoveIntersectionObserver(DOMIntersectionObserver* aObserver)
+{
+  mIntersectionObservers.RemoveElement(aObserver);
+}
+
+void
+nsDocument::UpdateIntersectionObservations()
+{
+  DOMHighResTimeStamp time = 0;
+  if (nsPIDOMWindowInner* window = GetInnerWindow()) {
+    Performance* perf = window->GetPerformance();
+    if (perf) {
+      time = perf->Now();
+    }
+  }
+  for (const auto& observer : mIntersectionObservers) {
+    observer->Update(this, time);
+  }
+}
+
+void
+nsDocument::ScheduleIntersectionObserverNotification()
+{
+  nsCOMPtr<nsIRunnable> notification = NewRunnableMethod(this,
+    &nsDocument::NotifyIntersectionObservers);
+  NS_DispatchToCurrentThread(notification);
+}
+
+void
+nsDocument::NotifyIntersectionObservers()
+{
+  for (const auto& observer : mIntersectionObservers) {
+    observer->Notify();
+  }
+}
+
 XPathEvaluator*
 nsIDocument::XPathEvaluator()
 {
   if (!mXPathEvaluator) {
     mXPathEvaluator = new dom::XPathEvaluator(this);
   }
   return mXPathEvaluator;
 }
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -64,16 +64,17 @@
 #include "nsDataHashtable.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/Attributes.h"
 #include "nsIDOMXPathEvaluator.h"
 #include "jsfriendapi.h"
 #include "ImportManager.h"
 #include "mozilla/LinkedList.h"
 #include "CustomElementRegistry.h"
+#include "mozilla/dom/Performance.h"
 
 #define XML_DECLARATION_BITS_DECLARATION_EXISTS   (1 << 0)
 #define XML_DECLARATION_BITS_ENCODING_EXISTS      (1 << 1)
 #define XML_DECLARATION_BITS_STANDALONE_EXISTS    (1 << 2)
 #define XML_DECLARATION_BITS_STANDALONE_YES       (1 << 3)
 
 
 class nsDOMStyleSheetSetList;
@@ -92,16 +93,18 @@ class nsPIBoxObject;
 
 namespace mozilla {
 class EventChainPreVisitor;
 namespace dom {
 class BoxObject;
 class ImageTracker;
 struct LifecycleCallbacks;
 class CallbackFunction;
+class DOMIntersectionObserver;
+class Performance;
 
 struct FullscreenRequest : public LinkedListElement<FullscreenRequest>
 {
   explicit FullscreenRequest(Element* aElement);
   FullscreenRequest(const FullscreenRequest&) = delete;
   ~FullscreenRequest();
 
   Element* GetElement() const { return mElement; }
@@ -768,16 +771,25 @@ public:
   // for radio group
   nsRadioGroupStruct* GetRadioGroup(const nsAString& aName) const;
   nsRadioGroupStruct* GetOrCreateRadioGroup(const nsAString& aName);
 
   virtual nsViewportInfo GetViewportInfo(const mozilla::ScreenIntSize& aDisplaySize) override;
 
   void ReportUseCounters();
 
+  virtual void AddIntersectionObserver(
+    mozilla::dom::DOMIntersectionObserver* aObserver) override;
+  virtual void RemoveIntersectionObserver(
+    mozilla::dom::DOMIntersectionObserver* aObserver) override;
+  virtual void UpdateIntersectionObservations() override;
+  virtual void ScheduleIntersectionObserverNotification() override;
+  virtual void NotifyIntersectionObservers() override;
+
+
 private:
   void AddOnDemandBuiltInUASheet(mozilla::StyleSheet* aSheet);
   nsRadioGroupStruct* GetRadioGroupInternal(const nsAString& aName) const;
   void SendToConsole(nsCOMArray<nsISecurityConsoleMessage>& aMessages);
 
 public:
   // nsIDOMNode
   NS_FORWARD_NSIDOMNODE_TO_NSINODE_OVERRIDABLE
@@ -1320,16 +1332,19 @@ protected:
 
   nsTArray<RefPtr<mozilla::StyleSheet>> mStyleSheets;
   nsTArray<RefPtr<mozilla::StyleSheet>> mOnDemandBuiltInUASheets;
   nsTArray<RefPtr<mozilla::StyleSheet>> mAdditionalSheets[AdditionalSheetTypeCount];
 
   // Array of observers
   nsTObserverArray<nsIDocumentObserver*> mObservers;
 
+  // Array of intersection observers
+  nsTArray<RefPtr<mozilla::dom::DOMIntersectionObserver>> mIntersectionObservers;
+
   // Tracker for animations that are waiting to start.
   // nullptr until GetOrCreatePendingAnimationTracker is called.
   RefPtr<mozilla::PendingAnimationTracker> mPendingAnimationTracker;
 
   // 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;
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -122,16 +122,17 @@ class Attr;
 class BoxObject;
 class CDATASection;
 class Comment;
 struct CustomElementDefinition;
 class DocumentFragment;
 class DocumentTimeline;
 class DocumentType;
 class DOMImplementation;
+class DOMIntersectionObserver;
 class DOMStringList;
 class Element;
 struct ElementCreationOptions;
 struct ElementRegistrationOptions;
 class Event;
 class EventTarget;
 class FontFaceSet;
 class FrameRequestCallback;
@@ -2839,16 +2840,25 @@ public:
   bool HasScriptsBlockedBySandbox();
 
   void ReportHasScrollLinkedEffect();
   bool HasScrollLinkedEffect() const
   {
     return mHasScrollLinkedEffect;
   }
 
+  virtual void AddIntersectionObserver(
+    mozilla::dom::DOMIntersectionObserver* aObserver) = 0;
+  virtual void RemoveIntersectionObserver(
+    mozilla::dom::DOMIntersectionObserver* aObserver) = 0;
+  
+  virtual void UpdateIntersectionObservations() = 0;
+  virtual void ScheduleIntersectionObserverNotification() = 0;
+  virtual void NotifyIntersectionObservers() = 0;
+
 protected:
   bool GetUseCounter(mozilla::UseCounter aUseCounter)
   {
     return mUseCounters[aUseCounter];
   }
 
   void SetChildDocumentUseCounter(mozilla::UseCounter aUseCounter)
   {
new file mode 100644
--- /dev/null
+++ b/dom/base/test/intersectionobserver_iframe.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<style>
+#target5 {
+        position: absolute;
+        top: 0px;
+        left: 0px;
+        width: 20px;
+        height: 20px;
+        background: #f00;
+}
+</style>
+<body>
+<div id="target5"></div>
+<script>
+        var io = new IntersectionObserver(function (records) {
+                window.parent.postMessage(records[0].rootBounds == null, 'http://mochi.test:8888');
+        }, {});
+        io.observe(document.getElementById("target5"));
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/base/test/intersectionobserver_window.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<style>
+#target5 {
+        position: absolute;
+        top: 0px;
+        left: 0px;
+        width: 20px;
+        height: 20px;
+        background: #f00;
+}
+</style>
+<body>
+<div id="target"></div>
+<script>
+        var io = new IntersectionObserver(function(records) {
+          var viewportWidth =
+              document.documentElement.clientWidth || document.body.clientWidth;
+          var viewportHeight =
+              document.documentElement.clientHeight || document.body.clientHeight;
+          var passed = records.length === 1 &&
+                       records[0].rootBounds.top === 0 &&
+                       records[0].rootBounds.left === 0 &&
+                       records[0].rootBounds.right === viewportWidth &&
+                       records[0].rootBounds.width === viewportWidth &&
+                       records[0].rootBounds.bottom === viewportHeight &&
+                       records[0].rootBounds.height === viewportHeight;
+          window.opener.postMessage(passed, '*');
+        });
+        io.observe(document.getElementById("target"));
+</script>
+</body>
+</html>
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -231,16 +231,18 @@ support-files =
   websocket_helpers.js
   websocket_tests.js
   !/dom/html/test/form_submit_server.sjs
   !/dom/security/test/cors/file_CrossSiteXHR_server.sjs
   !/image/test/mochitest/blue.png
   !/dom/xhr/tests/file_XHRSendData.sjs
   script_bug1238440.js
   file_blobURL_expiring.html
+  intersectionobserver_iframe.html
+  intersectionobserver_window.html
 
 [test_anchor_area_referrer.html]
 [test_anchor_area_referrer_changing.html]
 [test_anchor_area_referrer_invalid.html]
 [test_anchor_area_referrer_rel.html]
 [test_anonymousContent_api.html]
 [test_anonymousContent_append_after_reflow.html]
 [test_anonymousContent_canvas.html]
@@ -692,16 +694,17 @@ skip-if = e10s || os != 'linux' || build
 [test_htmlcopyencoder.xhtml]
 [test_iframe_referrer.html]
 [test_iframe_referrer_changing.html]
 [test_iframe_referrer_invalid.html]
 [test_Image_constructor.html]
 [test_img_referrer.html]
 [test_innersize_scrollport.html]
 [test_integer_attr_with_leading_zero.html]
+[test_intersectionobservers.html]
 [test_ipc_messagemanager_blob.html]
 [test_link_prefetch.html]
 skip-if = !e10s # Track Bug 1281415
 [test_link_stylesheet.html]
 [test_messagemanager_targetchain.html]
 [test_meta_viewport0.html]
 skip-if = (os != 'b2g' && os != 'android')    # meta-viewport tag support is mobile-only
 [test_meta_viewport1.html]
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_intersectionobservers.html
@@ -0,0 +1,1214 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1243846
+
+Some tests ported from IntersectionObserver/polyfill/intersection-observer-test.html
+
+Original license header:
+
+Copyright 2016 Google Inc. All Rights Reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1243846</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="next()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1243846">Mozilla Bug 1243846</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+  SpecialPowers.setIntPref("layout.visibility.min-notify-intersection-observers-interval-ms", 0);
+
+  var tests = [];
+  var curDescribeMsg = '';
+  var curItMsg = '';
+
+  function beforeEach_fn() { };
+  function afterEach_fn() { };
+
+  function before(fn) {
+    fn();
+  }
+
+  function beforeEach(fn) {
+    beforeEach_fn = fn;
+  }
+
+  function afterEach(fn) {
+    afterEach_fn = fn;
+  }
+
+  function it(msg, fn) {
+    tests.push({
+      msg: `${msg} [${curDescribeMsg}]`,
+      fn: fn
+    });
+  }
+
+  var callbacks = [];
+  function callDelayed(fn, delay) {
+    callbacks.push({
+      fn: fn,
+      time: +new Date() + delay
+    });
+  }
+
+  requestAnimationFrame(function tick() {
+    var i = callbacks.length;
+    while (i--) {
+      var cb = callbacks[i];
+      if (+new Date() >= cb.time) {
+        SimpleTest.executeSoon(cb.fn);
+        callbacks.splice(i, 1);
+      }
+    }
+    requestAnimationFrame(tick);
+  });
+
+  function expect(val) {
+    return {
+      to: {
+        throwException: function (regexp) {
+          try {
+            val();
+            ok(false, `${curItMsg} - an exception should have beeen thrown`);
+          } catch (e) {
+            ok(regexp.test(e), `${curItMsg} - supplied regexp should match thrown exception`);
+          }
+        },
+        get be() {
+          var fn = function (expected) {
+            is(val, expected, curItMsg);
+          };
+          fn.ok = function () {
+            ok(val, curItMsg);
+          };
+          fn.greaterThan = function (other) {
+            ok(val > other, `${curItMsg} - ${val} should be greater than ${other}`);
+          };
+          fn.lessThan = function (other) {
+            ok(val < other, `${curItMsg} - ${val} should be less than ${other}`);
+          };
+          return fn;
+        },
+        eql: function (expected) {
+          if (Array.isArray(expected)) {
+            if (!Array.isArray(val)) {
+              ok(false, curItMsg, `${curItMsg} - should be an array,`);
+              return;
+            }
+            is(val.length, expected.length, curItMsg, `${curItMsg} - arrays should be the same length`);
+            if (expected.length != val.length) {
+              return;
+            }
+            for (var i = 0; i < expected.length; i++) {
+              is(val[i], expected[i], `${curItMsg} - array elements at position ${i} should be equal`);
+              if (expected[i] != val[i]) {
+                return;
+              }
+            }
+            ok(true);
+          }
+        },
+      }
+    }
+  }
+
+  function describe(msg, fn) {
+    curDescribeMsg = msg;
+    fn();
+    curDescribeMsg = '';
+  }
+
+  function next() {
+    var test = tests.shift();
+    if (test) {
+      console.log(test.msg);
+      curItMsg = test.msg;
+      var fn = test.fn;
+      beforeEach_fn();
+      if (fn.length) {
+        fn(function () {
+          afterEach_fn();
+          next();
+        });
+      } else {
+        fn();
+        afterEach_fn();
+        next();
+      }
+    } else {
+      SimpleTest.finish();
+    }
+  }
+
+  var sinon = {
+    spy: function () {
+      var callbacks = [];
+      var fn = function () {
+        fn.callCount++;
+        fn.lastCall = { args: arguments };
+        if (callbacks.length) {
+          callbacks.shift()();
+        }
+      };
+      fn.callCount = 0;
+      fn.lastCall = { args: [] };
+      fn.waitForNotification = (fn) => {
+        callbacks.push(fn);
+      };
+      return fn;
+    }
+  };
+
+  var ASYNC_TIMEOUT = 300;
+
+
+  var io;
+  var noop = function() {};
+
+
+  // References to DOM elements, which are accessible to any test
+  // and reset prior to each test so state isn't shared.
+  var rootEl;
+  var grandParentEl;
+  var parentEl;
+  var targetEl1;
+  var targetEl2;
+  var targetEl3;
+  var targetEl4;
+  var targetEl5;
+
+
+  describe('IntersectionObserver', function() {
+
+    before(function() {
+
+    });
+
+
+    beforeEach(function() {
+      addStyles();
+      addFixtures();
+    });
+
+
+    afterEach(function() {
+      if (io && 'disconnect' in io) io.disconnect();
+      io = null;
+
+      window.onmessage = null;
+
+      removeStyles();
+      removeFixtures();
+    });
+
+
+    describe('constructor', function() {
+
+      it('throws when callback is not a function', function() {
+        expect(function() {
+          io = new IntersectionObserver(null);
+        }).to.throwException(/.*/i);
+      });
+
+
+      it('instantiates root correctly', function() {
+        io = new IntersectionObserver(noop);
+        expect(io.root).to.be(null);
+
+        io = new IntersectionObserver(noop, {root: rootEl});
+        expect(io.root).to.be(rootEl);
+      });
+
+
+      it('throws when root is not an Element', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop, {root: 'foo'});
+        }).to.throwException(/.*/i);
+      });
+
+
+      it('instantiates rootMargin correctly', function() {
+        io = new IntersectionObserver(noop, {rootMargin: '10px'});
+        expect(io.rootMargin).to.be('10px 10px 10px 10px');
+
+        io = new IntersectionObserver(noop, {rootMargin: '10px -5%'});
+        expect(io.rootMargin).to.be('10px -5% 10px -5%');
+
+        io = new IntersectionObserver(noop, {rootMargin: '10px 20% 0px'});
+        expect(io.rootMargin).to.be('10px 20% 0px 20%');
+
+        io = new IntersectionObserver(noop, {rootMargin: '0px 0px -5% 5px'});
+        expect(io.rootMargin).to.be('0px 0px -5% 5px');
+      });
+
+
+      it('throws when rootMargin is not in pixels or percent', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop, {rootMargin: 'auto'});
+        }).to.throwException(/pixels.*percent/i);
+      });
+
+
+      it('instantiates thresholds correctly', function() {
+        io = new IntersectionObserver(noop);
+        expect(io.thresholds).to.eql([0]);
+
+        io = new IntersectionObserver(noop, {threshold: 0.5});
+        expect(io.thresholds).to.eql([0.5]);
+
+        io = new IntersectionObserver(noop, {threshold: [0.25, 0.5, 0.75]});
+        expect(io.thresholds).to.eql([0.25, 0.5, 0.75]);
+
+        io = new IntersectionObserver(noop, {threshold: [1, .5, 0]});
+        expect(io.thresholds).to.eql([0, .5, 1]);
+      });
+
+      it('throws when a threshold value is not between 0 and 1', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop, {threshold: [0, -1]});
+        }).to.throwException(/threshold/i);
+      });
+
+      it('throws when a threshold value is not a number', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop, {threshold: "foo"});
+        }).to.throwException(/.*/i);
+      });
+
+    });
+
+
+    describe('observe', function() {
+
+      it('throws when target is not an Element', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop);
+          io.observe(null);
+        }).to.throwException(/.*/i);
+      });
+
+
+      it('triggers if target intersects when observing begins', function(done) {
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].intersectionRatio).to.be(1);
+          done();
+        }, {root: rootEl});
+        io.observe(targetEl1);
+      });
+
+
+      it('triggers with the correct arguments', function(done) {
+        io = new IntersectionObserver(function(records, observer) {
+          expect(records.length).to.be(1);
+          expect(records[0] instanceof IntersectionObserverEntry).to.be.ok();
+          expect(observer).to.be(io);
+          expect(this).to.be(io);
+          done();
+        }, {root: rootEl});
+        io.observe(targetEl1);
+      });
+
+
+      it('does not trigger if target does not intersect when observing begins',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        targetEl2.style.top = '-40px';
+        io.observe(targetEl2);
+        callDelayed(function() {
+          expect(spy.callCount).to.be(0);
+          done();
+        }, ASYNC_TIMEOUT);
+      });
+
+
+      it('does not trigger if target is not a descendant of the intersection root in the containing block chain',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: parentEl});
+
+        parentEl.style.position = 'static';
+        io.observe(targetEl2);
+        callDelayed(function() {
+          expect(spy.callCount).to.be(0);
+          done();
+        }, ASYNC_TIMEOUT);
+      });
+
+      it('triggers if target or root becomes invisible',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            io.observe(targetEl1);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.display = 'none';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.display = 'block';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            rootEl.style.display = 'none';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(4);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            rootEl.style.display = 'block';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(5);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+        ], done);
+      });
+
+
+      it('handles container elements with non-visible overflow',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            io.observe(targetEl1);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.left = '-40px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            parentEl.style.overflow = 'visible';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+      });
+
+
+      it('observes one target at a single threshold correctly', function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl, threshold: 0.5});
+
+        runSequence([
+          function(done) {
+            targetEl1.style.left = '-5px';
+            io.observe(targetEl1);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be.greaterThan(0.5);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.left = '-15px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be.lessThan(0.5);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.left = '-25px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(2);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.left = '-10px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0.5);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+
+      });
+
+
+      it('observes multiple targets at multiple thresholds correctly',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {
+          root: rootEl,
+          threshold: [1, 0.5, 0]
+        });
+
+        runSequence([
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '-15px';
+            targetEl2.style.top = '-5px';
+            targetEl2.style.left = '0px';
+            targetEl3.style.top = '0px';
+            targetEl3.style.left = '205px';
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.25);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(0.75);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '-5px';
+            targetEl2.style.top = '-15px';
+            targetEl2.style.left = '0px';
+            targetEl3.style.top = '0px';
+            targetEl3.style.left = '195px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(3);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.75);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(0.25);
+              expect(records[2].target).to.be(targetEl3);
+              expect(records[2].intersectionRatio).to.be(0.25);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '5px';
+            targetEl2.style.top = '-25px';
+            targetEl2.style.left = '0px';
+            targetEl3.style.top = '0px';
+            targetEl3.style.left = '185px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(3);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(0);
+              expect(records[2].target).to.be(targetEl3);
+              expect(records[2].intersectionRatio).to.be(0.75);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '15px';
+            targetEl2.style.top = '-35px';
+            targetEl2.style.left = '0px';
+            targetEl3.style.top = '0px';
+            targetEl3.style.left = '175px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(4);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].target).to.be(targetEl3);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+      });
+
+
+      it('handles rootMargin properly', function(done) {
+
+        parentEl.style.overflow = 'visible';
+        targetEl1.style.top = '0px';
+        targetEl1.style.left = '-20px';
+        targetEl2.style.top = '-20px';
+        targetEl2.style.left = '0px';
+        targetEl3.style.top = '0px';
+        targetEl3.style.left = '200px';
+        targetEl4.style.top = '180px';
+        targetEl4.style.left = '180px';
+
+        runSequence([
+          function(done) {
+            io = new IntersectionObserver(function(records) {
+              records = sortRecords(records);
+              expect(records.length).to.be(4);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(.5);
+              expect(records[2].target).to.be(targetEl3);
+              expect(records[2].intersectionRatio).to.be(.5);
+              expect(records[3].target).to.be(targetEl4);
+              expect(records[3].intersectionRatio).to.be(1);
+              io.disconnect();
+              done();
+            }, {root: rootEl, rootMargin: '10px'});
+
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+          },
+          function(done) {
+            io = new IntersectionObserver(function(records) {
+              records = sortRecords(records);
+              expect(records.length).to.be(3);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.5);
+              expect(records[1].target).to.be(targetEl3);
+              expect(records[1].intersectionRatio).to.be(0.5);
+              expect(records[2].target).to.be(targetEl4);
+              expect(records[2].intersectionRatio).to.be(0.5);
+              io.disconnect();
+              done();
+            }, {root: rootEl, rootMargin: '-10px 10%'});
+
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+          },
+          function(done) {
+            io = new IntersectionObserver(function(records) {
+              records = sortRecords(records);
+              expect(records.length).to.be(2);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.5);
+              expect(records[1].target).to.be(targetEl4);
+              expect(records[1].intersectionRatio).to.be(0.5);
+              io.disconnect();
+              done();
+            }, {root: rootEl, rootMargin: '-5% -2.5% 0px'});
+
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+          },
+          function(done) {
+            io = new IntersectionObserver(function(records) {
+              records = sortRecords(records);
+              expect(records.length).to.be(3);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.5);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(0.5);
+              expect(records[2].target).to.be(targetEl4);
+              expect(records[2].intersectionRatio).to.be(0.25);
+              io.disconnect();
+              done();
+            }, {root: rootEl, rootMargin: '5% -2.5% -10px -190px'});
+
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+          }
+        ], done);
+      });
+
+
+      it('handles targets on the boundary of root', function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '-21px';
+            targetEl2.style.top = '-20px';
+            targetEl2.style.left = '0px';
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl2);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '-20px';
+            targetEl2.style.top = '-21px';
+            targetEl2.style.left = '0px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[1].intersectionRatio).to.be(0);
+              expect(records[1].target).to.be(targetEl2);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '-20px';
+            targetEl1.style.left = '200px';
+            targetEl2.style.top = '200px';
+            targetEl2.style.left = '200px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl2);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl3.style.top = '20px';
+            targetEl3.style.left = '-20px';
+            targetEl4.style.top = '-20px';
+            targetEl4.style.left = '20px';
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(4);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl3);
+              expect(records[1].intersectionRatio).to.be(0);
+              expect(records[1].target).to.be(targetEl4);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+
+      });
+
+
+      it('handles zero-size targets within the root coordinate space',
+          function(done) {
+
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].intersectionRatio).to.be(0);
+          done();
+        }, {root: rootEl});
+
+        targetEl1.style.top = '0px';
+        targetEl1.style.left = '0px';
+        targetEl1.style.width = '0px';
+        targetEl1.style.height = '0px';
+        io.observe(targetEl1);
+      });
+
+
+      it('handles root/target elements not yet in the DOM', function(done) {
+
+        rootEl.parentNode.removeChild(rootEl);
+        targetEl1.parentNode.removeChild(targetEl1);
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            io.observe(targetEl1);
+            callDelayed(done, 0);
+          },
+          function(done) {
+            document.getElementById('fixtures').appendChild(rootEl);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            parentEl.insertBefore(targetEl1, targetEl2);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[0].target).to.be(targetEl1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            grandParentEl.parentNode.removeChild(grandParentEl);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            rootEl.appendChild(targetEl1);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[0].target).to.be(targetEl1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            rootEl.parentNode.removeChild(rootEl);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(4);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl1);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+      });
+
+
+      it('handles sub-root element scrolling', function(done) {
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].intersectionRatio).to.be(1);
+          done();
+        }, {root: rootEl});
+
+        io.observe(targetEl3);
+        callDelayed(function() {
+          parentEl.scrollLeft = 40;
+        }, 0);
+      });
+
+
+      it('supports CSS transitions and transforms', function(done) {
+
+        targetEl1.style.top = '220px';
+        targetEl1.style.left = '220px';
+
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].intersectionRatio).to.be(1);
+          done();
+        }, {root: rootEl, threshold: [1]});
+
+        io.observe(targetEl1);
+        callDelayed(function() {
+          targetEl1.style.transform = 'translateX(-40px) translateY(-40px)';
+        }, 0);
+      });
+
+
+      it('uses the viewport when no root is specified', function(done) {
+        window.onmessage = function (e) {
+          expect(e.data).to.be.ok();
+          win.close();
+          done();
+        };
+
+        var win = window.open("intersectionobserver_window.html");
+      });
+
+    });
+
+    describe('observe subframe', function () {
+      
+      it('should not trigger if target and root are not in the same document',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        targetEl4.onload = function () {
+          targetEl5 = targetEl4.contentDocument.getElementById('target5');
+          io.observe(targetEl5);
+          callDelayed(function() {
+            expect(spy.callCount).to.be(0);
+            done();
+          }, ASYNC_TIMEOUT);
+        }
+
+        targetEl4.src = "intersectionobserver_iframe.html";
+      
+      });
+
+      it('boundingClientRect matches target.getBoundingClientRect() for an element inside an iframe',
+          function(done) {
+
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].boundingClientRect.top, targetEl5.getBoundingClientRect().top);
+          expect(records[0].boundingClientRect.left, targetEl5.getBoundingClientRect().left);
+          expect(records[0].boundingClientRect.width, targetEl5.getBoundingClientRect().width);
+          expect(records[0].boundingClientRect.height, targetEl5.getBoundingClientRect().height);
+          done();
+        }, {threshold: [1]});
+
+        targetEl4.onload = function () {
+          targetEl5 = targetEl4.contentDocument.getElementById('target5');
+          io.observe(targetEl5);
+        }
+
+        targetEl4.src = "intersectionobserver_iframe.html";
+      });
+
+      it('rootBounds should is set to null for cross-origin observations', function(done) {
+
+        window.onmessage = function (e) {
+          expect(e.data).to.be.ok();
+          done();
+        };
+
+        targetEl4.src = "http://example.org/tests/dom/base/test/intersectionobserver_iframe.html";
+
+      });
+    
+    });
+
+    describe('takeRecords', function() {
+
+      it('supports getting records before the callback is invoked',
+          function(done) {
+
+        var lastestRecords = [];
+        io = new IntersectionObserver(function(records) {
+          lastestRecords = lastestRecords.concat(records);
+        }, {root: rootEl});
+        io.observe(targetEl1);
+
+        window.requestAnimationFrame && requestAnimationFrame(function() {
+          lastestRecords = lastestRecords.concat(io.takeRecords());
+        });
+
+        callDelayed(function() {
+          expect(lastestRecords.length).to.be(1);
+          expect(lastestRecords[0].intersectionRatio).to.be(1);
+          done();
+        }, ASYNC_TIMEOUT);
+      });
+
+    });
+
+
+    describe('unobserve', function() {
+
+      it('removes targets from the internal store', function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '0px';
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            io.unobserve(targetEl1);
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '-40px';
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].target).to.be(targetEl2);
+              expect(records[0].intersectionRatio).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            io.unobserve(targetEl2);
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '0px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(2);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+
+      });
+
+    });
+
+    describe('disconnect', function() {
+
+      it('removes all targets and stops listening for changes', function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '0px';
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            spy.waitForNotification(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            io.disconnect();
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '-40px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+
+      });
+
+    });
+
+  });
+
+
+  /**
+   * Runs a sequence of function and when finished invokes the done callback.
+   * Each function in the sequence is invoked with its own done function and
+   * it should call that function once it's complete.
+   * @param {Array<Function>} functions An array of async functions.
+   * @param {Function} done A final callback to be invoked once all function
+   *     have run.
+   */
+  function runSequence(functions, done) {
+    var next = functions.shift();
+    if (next) {
+      next(function() {
+        runSequence(functions, done);
+      });
+    } else {
+      done && done();
+    }
+  }
+
+
+  /**
+   * Sorts an array of records alphebetically by ascending ID. Since the current
+   * native implementation doesn't sort change entries by `observe` order, we do
+   * that ourselves for the non-polyfill case. Since all tests call observe
+   * on targets in sequential order, this should always match.
+   * https://crbug.com/613679
+   * @param {Array<IntersectionObserverEntry>} entries The entries to sort.
+   * @return {Array<IntersectionObserverEntry>} The sorted array.
+   */
+  function sortRecords(entries) {
+    entries = entries.sort(function(a, b) {
+      return a.target.id < b.target.id ? -1 : 1;
+    });
+    return entries;
+  }
+
+
+  /**
+   * Adds the common styles used by all tests to the page.
+   */
+  function addStyles() {
+    var styles = document.createElement('style');
+    styles.id = 'styles';
+    document.documentElement.appendChild(styles);
+
+    var cssText =
+        '#root {' +
+        '  position: relative;' +
+        '  width: 400px;' +
+        '  height: 200px;' +
+        '  background: #eee' +
+        '}' +
+        '#grand-parent {' +
+        '  position: relative;' +
+        '  width: 200px;' +
+        '  height: 200px;' +
+        '}' +
+        '#parent {' +
+        '  position: absolute;' +
+        '  top: 0px;' +
+        '  left: 200px;' +
+        '  overflow: hidden;' +
+        '  width: 200px;' +
+        '  height: 200px;' +
+        '  background: #ddd;' +
+        '}' +
+        '#target1, #target2, #target3, #target4 {' +
+        '  position: absolute;' +
+        '  top: 0px;' +
+        '  left: 0px;' +
+        '  width: 20px;' +
+        '  height: 20px;' +
+        '  transform: translateX(0px) translateY(0px);' +
+        '  transition: transform .5s;' +
+        '  background: #f00;' +
+        '  border: none;' +
+        '}';
+
+    styles.innerHTML = cssText;
+  }
+
+
+  /**
+   * Adds the DOM fixtures used by all tests to the page and assigns them to
+   * global variables so they can be referenced within the tests.
+   */
+  function addFixtures() {
+    var fixtures = document.createElement('div');
+    fixtures.id = 'fixtures';
+
+    fixtures.innerHTML =
+        '<div id="root">' +
+        '  <div id="grand-parent">' +
+        '    <div id="parent">' +
+        '      <div id="target1"></div>' +
+        '      <div id="target2"></div>' +
+        '      <div id="target3"></div>' +
+        '      <iframe id="target4"></iframe>' +
+        '    </div>' +
+        '  </div>' +
+        '</div>';
+
+    document.body.appendChild(fixtures);
+
+    rootEl = document.getElementById('root');
+    grandParentEl = document.getElementById('grand-parent');
+    parentEl = document.getElementById('parent');
+    targetEl1 = document.getElementById('target1');
+    targetEl2 = document.getElementById('target2');
+    targetEl3 = document.getElementById('target3');
+    targetEl4 = document.getElementById('target4');
+  }
+
+
+  /**
+   * Removes the common styles from the page.
+   */
+  function removeStyles() {
+    var styles = document.getElementById('styles');
+    styles.parentNode.removeChild(styles);
+  }
+
+
+  /**
+   * Removes the DOM fixtures from the page and resets the global references.
+   */
+  function removeFixtures() {
+    var fixtures = document.getElementById('fixtures');
+    fixtures.parentNode.removeChild(fixtures);
+
+    rootEl = null;
+    grandParentEl = null;
+    parentEl = null;
+    targetEl1 = null;
+    targetEl2 = null;
+    targetEl3 = null;
+    targetEl4 = null;
+  }
+
+  SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+<div id="log">
+</div>
+</body>
+</html>
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -576,16 +576,25 @@ DOMInterfaces = {
     'wrapperCache': False,
 },
 
 'InputStream': {
     'nativeType': 'nsIInputStream',
     'notflattened': True
 },
 
+'IntersectionObserver': {
+    'nativeType': 'mozilla::dom::DOMIntersectionObserver',
+},
+
+'IntersectionObserverEntry': {
+    'nativeType': 'mozilla::dom::DOMIntersectionObserverEntry',
+    'headerFile': 'DOMIntersectionObserver.h',
+},
+
 'KeyEvent': {
     'concrete': False
 },
 
 'KeyframeEffect': {
     'implicitJSContext': { 'setterOnly': [ 'spacing' ] }
 },
 
--- a/dom/bindings/Errors.msg
+++ b/dom/bindings/Errors.msg
@@ -98,8 +98,9 @@ MSG_DEF(MSG_SW_UPDATE_BAD_REGISTRATION, 
 MSG_DEF(MSG_INVALID_DURATION_ERROR, 1, JSEXN_TYPEERR, "Invalid duration '{0}'.")
 MSG_DEF(MSG_INVALID_EASING_ERROR, 1, JSEXN_TYPEERR, "Invalid easing '{0}'.")
 MSG_DEF(MSG_INVALID_SPACING_MODE_ERROR, 1, JSEXN_TYPEERR, "Invalid spacing '{0}'.")
 MSG_DEF(MSG_USELESS_SETTIMEOUT, 1, JSEXN_TYPEERR, "Useless {0} call (missing quotes around argument?)")
 MSG_DEF(MSG_TOKENLIST_NO_SUPPORTED_TOKENS, 2, JSEXN_TYPEERR, "{0} attribute of <{1}> does not define any supported tokens")
 MSG_DEF(MSG_CACHE_STREAM_CLOSED, 0, JSEXN_TYPEERR, "Response body is a cache file stream that has already been closed.")
 MSG_DEF(MSG_TIME_VALUE_OUT_OF_RANGE, 1, JSEXN_TYPEERR, "{0} is outside the supported range for time values.")
 MSG_DEF(MSG_ONLY_IF_CACHED_WITHOUT_SAME_ORIGIN, 1, JSEXN_TYPEERR, "Request mode '{0}' was used, but request cache mode 'only-if-cached' can only be used with request mode 'same-origin'.")
+MSG_DEF(MSG_THRESHOLD_RANGE_ERROR, 0, JSEXN_RANGEERR, "Threshold values must all be in the range [0, 1].")
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -606,16 +606,20 @@ var interfaceNamesInGlobalScope =
     {name: "ImageCaptureErrorEvent", disabled: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ImageData",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "InputEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "InstallTrigger",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "IntersectionObserver",
+// IMPORTANT: Do not change this list without review from a DOM peer!
+    "IntersectionObserverEntry",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "KeyEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "KeyboardEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "KeyframeEffectReadOnly", release: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "KeyframeEffect", release: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/webidl/DOMRect.webidl
+++ b/dom/webidl/DOMRect.webidl
@@ -24,9 +24,16 @@ interface DOMRectReadOnly {
     readonly attribute unrestricted double x;
     readonly attribute unrestricted double y;
     readonly attribute unrestricted double width;
     readonly attribute unrestricted double height;
     readonly attribute unrestricted double top;
     readonly attribute unrestricted double right;
     readonly attribute unrestricted double bottom;
     readonly attribute unrestricted double left;
-};
\ No newline at end of file
+};
+
+dictionary DOMRectInit {
+    unrestricted double x = 0;
+    unrestricted double y = 0;
+    unrestricted double width = 0;
+    unrestricted double height = 0;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/IntersectionObserver.webidl
@@ -0,0 +1,59 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origin of this IDL file is
+ * https://wicg.github.io/IntersectionObserver/
+ */
+
+[ProbablyShortLivingObject]
+interface IntersectionObserverEntry {
+  [Constant]
+  readonly attribute DOMHighResTimeStamp time;
+  [Constant]
+  readonly attribute DOMRectReadOnly? rootBounds;
+  [Constant]
+  readonly attribute DOMRectReadOnly boundingClientRect;
+  [Constant]
+  readonly attribute DOMRectReadOnly intersectionRect;
+  [Constant]
+  readonly attribute double intersectionRatio;
+  [Constant]
+  readonly attribute Element target;
+};
+
+[Constructor(IntersectionCallback intersectionCallback,
+             optional IntersectionObserverInit options)]
+interface IntersectionObserver {
+  [Constant]
+  readonly attribute Element? root;
+  [Constant]
+  readonly attribute DOMString rootMargin;
+  [Constant,Cached]
+  readonly attribute sequence<double> thresholds;
+  void observe(Element target);
+  void unobserve(Element target);
+  void disconnect();
+  sequence<IntersectionObserverEntry> takeRecords();
+
+  [ChromeOnly]
+  readonly attribute IntersectionCallback intersectionCallback;
+};
+
+callback IntersectionCallback =
+  void (sequence<IntersectionObserverEntry> entries, IntersectionObserver observer);
+
+dictionary IntersectionObserverEntryInit {
+  required DOMHighResTimeStamp time;
+  required DOMRectInit rootBounds;
+  required DOMRectInit boundingClientRect;
+  required DOMRectInit intersectionRect;
+  required Element target;
+};
+
+dictionary IntersectionObserverInit {
+  Element?  root = null;
+  DOMString rootMargin = "0px";
+  (double or sequence<double>) threshold = 0;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -275,16 +275,17 @@ WEBIDL_FILES = [
     'ImageCapture.webidl',
     'ImageData.webidl',
     'ImageDocument.webidl',
     'InputEvent.webidl',
     'InputMethod.webidl',
     'InputPort.webidl',
     'InputPortManager.webidl',
     'InspectorUtils.webidl',
+    'IntersectionObserver.webidl',
     'IterableIterator.webidl',
     'KeyAlgorithm.webidl',
     'KeyboardEvent.webidl',
     'KeyEvent.webidl',
     'KeyframeAnimationOptions.webidl',
     'KeyframeEffect.webidl',
     'KeyIdsInitData.webidl',
     'LegacyQueryInterface.webidl',
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -75,16 +75,17 @@ using namespace mozilla::widget;
 using namespace mozilla::ipc;
 using namespace mozilla::layout;
 
 static mozilla::LazyLogModule sRefreshDriverLog("nsRefreshDriver");
 #define LOG(...) MOZ_LOG(sRefreshDriverLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
 
 #define DEFAULT_THROTTLED_FRAME_RATE 1
 #define DEFAULT_RECOMPUTE_VISIBILITY_INTERVAL_MS 1000
+#define DEFAULT_NOTIFY_INTERSECTION_OBSERVERS_INTERVAL_MS 100
 // after 10 minutes, stop firing off inactive timers
 #define DEFAULT_INACTIVE_TIMER_DISABLE_SECONDS 600
 
 // The number of seconds spent skipping frames because we are waiting for the compositor
 // before logging.
 #if defined(MOZ_ASAN)
 # define REFRESH_WAIT_WARNING 5
 #elif defined(DEBUG) && !defined(MOZ_VALGRIND)
@@ -971,16 +972,27 @@ nsRefreshDriver::GetMinRecomputeVisibili
   int32_t interval =
     Preferences::GetInt("layout.visibility.min-recompute-interval-ms", -1);
   if (interval <= 0) {
     interval = DEFAULT_RECOMPUTE_VISIBILITY_INTERVAL_MS;
   }
   return TimeDuration::FromMilliseconds(interval);
 }
 
+/* static */ mozilla::TimeDuration
+nsRefreshDriver::GetMinNotifyIntersectionObserversInterval()
+{
+  int32_t interval =
+    Preferences::GetInt("layout.visibility.min-notify-intersection-observers-interval-ms", -1);
+  if (interval <= 0) {
+    interval = DEFAULT_NOTIFY_INTERSECTION_OBSERVERS_INTERVAL_MS;
+  }
+  return TimeDuration::FromMilliseconds(interval);
+}
+
 double
 nsRefreshDriver::GetRefreshTimerInterval() const
 {
   return mThrottled ? GetThrottledTimerInterval() : GetRegularTimerInterval();
 }
 
 RefreshDriverTimer*
 nsRefreshDriver::ChooseTimer() const
@@ -1013,16 +1025,18 @@ nsRefreshDriver::nsRefreshDriver(nsPresC
     mPresContext(aPresContext),
     mRootRefresh(nullptr),
     mPendingTransaction(0),
     mCompletedTransaction(0),
     mFreezeCount(0),
     mThrottledFrameRequestInterval(TimeDuration::FromMilliseconds(
                                      GetThrottledTimerInterval())),
     mMinRecomputeVisibilityInterval(GetMinRecomputeVisibilityInterval()),
+    mMinNotifyIntersectionObserversInterval(
+      GetMinNotifyIntersectionObserversInterval()),
     mThrottled(false),
     mNeedToRecomputeVisibility(false),
     mTestControllingRefreshes(false),
     mViewManagerFlushIsPending(false),
     mRequestedHighPrecision(false),
     mInRefresh(false),
     mWaitingForTransaction(false),
     mSkippedPaints(false),
@@ -1033,16 +1047,17 @@ nsRefreshDriver::nsRefreshDriver(nsPresC
   MOZ_ASSERT(mPresContext,
              "Need a pres context to tell us to call Disconnect() later "
              "and decrement sRefreshDriverCount.");
   mMostRecentRefreshEpochTime = JS_Now();
   mMostRecentRefresh = TimeStamp::Now();
   mMostRecentTick = mMostRecentRefresh;
   mNextThrottledFrameRequestTick = mMostRecentTick;
   mNextRecomputeVisibilityTick = mMostRecentTick;
+  mNextNotifyIntersectionObserversTick = mMostRecentTick;
 
   ++sRefreshDriverCount;
 }
 
 nsRefreshDriver::~nsRefreshDriver()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(ObserverCount() == 0,
@@ -1823,16 +1838,32 @@ nsRefreshDriver::Tick(int64_t aNowEpoch,
       aNowTime >= mNextRecomputeVisibilityTick &&
       !presShell->IsPaintingSuppressed()) {
     mNextRecomputeVisibilityTick = aNowTime + mMinRecomputeVisibilityInterval;
     mNeedToRecomputeVisibility = false;
 
     presShell->ScheduleApproximateFrameVisibilityUpdateNow();
   }
 
+  bool notifyIntersectionObservers = false;
+  if (aNowTime >= mNextNotifyIntersectionObserversTick) {
+    mNextNotifyIntersectionObserversTick =
+      aNowTime + mMinNotifyIntersectionObserversInterval;
+    notifyIntersectionObservers = true;
+  }
+  nsCOMArray<nsIDocument> documents;
+  CollectDocuments(mPresContext->Document(), &documents);
+  for (int32_t i = 0; i < documents.Count(); ++i) {
+    nsIDocument* doc = documents[i];
+    doc->UpdateIntersectionObservations();
+    if (notifyIntersectionObservers) {
+      doc->ScheduleIntersectionObserverNotification();
+    }
+  }
+
   /*
    * Perform notification to imgIRequests subscribed to listen
    * for refresh events.
    */
 
   for (auto iter = mStartTable.Iter(); !iter.Done(); iter.Next()) {
     const uint32_t& delay = iter.Key();
     ImageStartData* data = iter.UserData();
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -363,16 +363,17 @@ private:
   // Trigger a refresh immediately, if haven't been disconnected or frozen.
   void DoRefresh();
 
   double GetRefreshTimerInterval() const;
   double GetRegularTimerInterval(bool *outIsDefault = nullptr) const;
   static double GetThrottledTimerInterval();
 
   static mozilla::TimeDuration GetMinRecomputeVisibilityInterval();
+  static mozilla::TimeDuration GetMinNotifyIntersectionObserversInterval();
 
   bool HaveFrameRequestCallbacks() const {
     return mFrameRequestCallbackDocs.Length() != 0;
   }
 
   void FinishedWaitingForTransaction();
 
   mozilla::RefreshDriverTimer* ChooseTimer() const;
@@ -398,16 +399,18 @@ private:
   const mozilla::TimeDuration mThrottledFrameRequestInterval;
 
   // How long we wait, at a minimum, before recomputing approximate frame
   // visibility information. This is a minimum because, regardless of this
   // interval, we only recompute visibility when we've seen a layout or style
   // flush since the last time we did it.
   const mozilla::TimeDuration mMinRecomputeVisibilityInterval;
 
+  const mozilla::TimeDuration mMinNotifyIntersectionObserversInterval;
+
   bool mThrottled;
   bool mNeedToRecomputeVisibility;
   bool mTestControllingRefreshes;
   bool mViewManagerFlushIsPending;
   bool mRequestedHighPrecision;
   bool mInRefresh;
 
   // True if the refresh driver is suspended waiting for transaction
@@ -428,16 +431,17 @@ private:
   // transaction to be completed before we append a note to the gfx critical log.
   // The number is doubled every time the threshold is hit.
   uint64_t mWarningThreshold;
   mozilla::TimeStamp mMostRecentRefresh;
   mozilla::TimeStamp mMostRecentTick;
   mozilla::TimeStamp mTickStart;
   mozilla::TimeStamp mNextThrottledFrameRequestTick;
   mozilla::TimeStamp mNextRecomputeVisibilityTick;
+  mozilla::TimeStamp mNextNotifyIntersectionObserversTick;
 
   // separate arrays for each flush type we support
   ObserverArray mObservers[3];
   RequestTable mRequests;
   ImageStartTable mStartTable;
 
   struct PendingEvent {
     nsCOMPtr<nsINode> mTarget;
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -227,16 +227,22 @@ public:
                                  nsCSSValue& aValue);
 
   bool ParseColorString(const nsSubstring& aBuffer,
                         nsIURI* aURL, // for error reporting
                         uint32_t aLineNumber, // for error reporting
                         nsCSSValue& aValue,
                         bool aSuppressErrors /* false */);
 
+  bool ParseMarginString(const nsSubstring& aBuffer,
+                         nsIURI* aURL, // for error reporting
+                         uint32_t aLineNumber, // for error reporting
+                         nsCSSValue& aValue,
+                         bool aSuppressErrors /* false */);
+
   nsresult ParseSelectorString(const nsSubstring& aSelectorString,
                                nsIURI* aURL, // for error reporting
                                uint32_t aLineNumber, // for error reporting
                                nsCSSSelectorList **aSelectorList);
 
   already_AddRefed<nsCSSKeyframeRule>
   ParseKeyframeRule(const nsSubstring& aBuffer,
                     nsIURI*            aURL,
@@ -1153,17 +1159,18 @@ protected:
    * Calls AppendImpliedEOFCharacters on mScanner.
    */
   void AppendImpliedEOFCharacters(nsAString& aResult);
 
   // Reused utility parsing routines
   void AppendValue(nsCSSPropertyID aPropID, const nsCSSValue& aValue);
   bool ParseBoxProperties(const nsCSSPropertyID aPropIDs[]);
   bool ParseGroupedBoxProperty(int32_t aVariantMask,
-                               nsCSSValue& aValue);
+                               nsCSSValue& aValue,
+                               uint32_t aRestrictions);
   bool ParseBoxCornerRadius(const nsCSSPropertyID aPropID);
   bool ParseBoxCornerRadiiInternals(nsCSSValue array[]);
   bool ParseBoxCornerRadii(const nsCSSPropertyID aPropIDs[]);
 
   int32_t ParseChoice(nsCSSValue aValues[],
                       const nsCSSPropertyID aPropIDs[], int32_t aNumIDs);
 
   CSSParseResult ParseColor(nsCSSValue& aValue);
@@ -2268,16 +2275,42 @@ CSSParserImpl::ParseColorString(const ns
     OUTPUT_ERROR();
   }
 
   ReleaseScanner();
   return colorParsed;
 }
 
 bool
+CSSParserImpl::ParseMarginString(const nsSubstring& aBuffer,
+                                 nsIURI* aURI, // for error reporting
+                                 uint32_t aLineNumber, // for error reporting
+                                 nsCSSValue& aValue,
+                                 bool aSuppressErrors /* false */)
+{
+  nsCSSScanner scanner(aBuffer, aLineNumber);
+  css::ErrorReporter reporter(scanner, mSheet, mChildLoader, aURI);
+  InitScanner(scanner, reporter, aURI, aURI, nullptr);
+
+  nsAutoSuppressErrors suppressErrors(this, aSuppressErrors);
+
+  // Parse a margin, and check that there's nothing else after it.
+  bool marginParsed = ParseGroupedBoxProperty(VARIANT_LP, aValue, 0) && !GetToken(true);
+
+  if (aSuppressErrors) {
+    CLEAR_ERROR();
+  } else {
+    OUTPUT_ERROR();
+  }
+
+  ReleaseScanner();
+  return marginParsed;
+}
+
+bool
 CSSParserImpl::ParseFontFamilyListString(const nsSubstring& aBuffer,
                                          nsIURI* aURI, // for error reporting
                                          uint32_t aLineNumber, // for error reporting
                                          nsCSSValue& aValue)
 {
   nsCSSScanner scanner(aBuffer, aLineNumber);
   css::ErrorReporter reporter(scanner, mSheet, mChildLoader, aURI);
   InitScanner(scanner, reporter, aURI, aURI, nullptr);
@@ -11179,29 +11212,30 @@ CSSParserImpl::ParseBoxProperties(const 
 
   NS_FOR_CSS_SIDES (index) {
     AppendValue(aPropIDs[index], result.*(nsCSSRect::sides[index]));
   }
   return true;
 }
 
 // Similar to ParseBoxProperties, except there is only one property
-// with the result as its value, not four. Requires values be nonnegative.
+// with the result as its value, not four.
 bool
 CSSParserImpl::ParseGroupedBoxProperty(int32_t aVariantMask,
-                                       /** outparam */ nsCSSValue& aValue)
+                                       /** outparam */ nsCSSValue& aValue,
+                                       uint32_t aRestrictions)
 {
   nsCSSRect& result = aValue.SetRectValue();
 
   int32_t count = 0;
   NS_FOR_CSS_SIDES (index) {
     CSSParseResult parseResult =
       ParseVariantWithRestrictions(result.*(nsCSSRect::sides[index]),
                                    aVariantMask, nullptr,
-                                   CSS_PROPERTY_VALUE_NONNEGATIVE);
+                                   aRestrictions);
     if (parseResult == CSSParseResult::NotFound) {
       break;
     }
     if (parseResult == CSSParseResult::Error) {
       return false;
     }
     count++;
   }
@@ -13205,17 +13239,18 @@ CSSParserImpl::ParseBorderImageSlice(boo
 
   // Try parsing "fill" value.
   nsCSSValue imageSliceFillValue;
   bool hasFill = ParseEnum(imageSliceFillValue,
                            nsCSSProps::kBorderImageSliceKTable);
 
   // Parse the box dimensions.
   nsCSSValue imageSliceBoxValue;
-  if (!ParseGroupedBoxProperty(VARIANT_PN, imageSliceBoxValue)) {
+  if (!ParseGroupedBoxProperty(VARIANT_PN, imageSliceBoxValue,
+                               CSS_PROPERTY_VALUE_NONNEGATIVE)) {
     if (!hasFill && aConsumedTokens) {
       *aConsumedTokens = false;
     }
 
     return false;
   }
 
   // Try parsing "fill" keyword again if the first time failed because keyword
@@ -13249,17 +13284,17 @@ CSSParserImpl::ParseBorderImageWidth(boo
       ParseSingleTokenVariant(value, VARIANT_INHERIT, nullptr)) {
     // Keywords "inherit", "initial" and "unset" can not be mixed, so we
     // are done.
     AppendValue(eCSSProperty_border_image_width, value);
     return true;
   }
 
   // Parse the box dimensions.
-  if (!ParseGroupedBoxProperty(VARIANT_ALPN, value)) {
+  if (!ParseGroupedBoxProperty(VARIANT_ALPN, value, CSS_PROPERTY_VALUE_NONNEGATIVE)) {
     return false;
   }
 
   AppendValue(eCSSProperty_border_image_width, value);
   return true;
 }
 
 bool
@@ -13272,17 +13307,17 @@ CSSParserImpl::ParseBorderImageOutset(bo
       ParseSingleTokenVariant(value, VARIANT_INHERIT, nullptr)) {
     // Keywords "inherit", "initial" and "unset" can not be mixed, so we
     // are done.
     AppendValue(eCSSProperty_border_image_outset, value);
     return true;
   }
 
   // Parse the box dimensions.
-  if (!ParseGroupedBoxProperty(VARIANT_LN, value)) {
+  if (!ParseGroupedBoxProperty(VARIANT_LN, value, CSS_PROPERTY_VALUE_NONNEGATIVE)) {
     return false;
   }
 
   AppendValue(eCSSProperty_border_image_outset, value);
   return true;
 }
 
 bool
@@ -17989,16 +18024,27 @@ nsCSSParser::ParseColorString(const nsSu
                               uint32_t           aLineNumber,
                               nsCSSValue&        aValue,
                               bool               aSuppressErrors /* false */)
 {
   return static_cast<CSSParserImpl*>(mImpl)->
     ParseColorString(aBuffer, aURI, aLineNumber, aValue, aSuppressErrors);
 }
 
+bool
+nsCSSParser::ParseMarginString(const nsSubstring& aBuffer,
+                               nsIURI*            aURI,
+                               uint32_t           aLineNumber,
+                               nsCSSValue&        aValue,
+                               bool               aSuppressErrors /* false */)
+{
+  return static_cast<CSSParserImpl*>(mImpl)->
+    ParseMarginString(aBuffer, aURI, aLineNumber, aValue, aSuppressErrors);
+}
+
 nsresult
 nsCSSParser::ParseSelectorString(const nsSubstring&  aSelectorString,
                                  nsIURI*             aURI,
                                  uint32_t            aLineNumber,
                                  nsCSSSelectorList** aSelectorList)
 {
   return static_cast<CSSParserImpl*>(mImpl)->
     ParseSelectorString(aSelectorString, aURI, aLineNumber, aSelectorList);
--- a/layout/style/nsCSSParser.h
+++ b/layout/style/nsCSSParser.h
@@ -201,16 +201,28 @@ public:
    */
   bool ParseColorString(const nsSubstring& aBuffer,
                         nsIURI*            aURL,
                         uint32_t           aLineNumber,
                         nsCSSValue&        aValue,
                         bool               aSuppressErrors = false);
 
   /**
+   * Parse aBuffer into a nsCSSValue |aValue|. Will return false
+   * if aBuffer is not a valid CSS margin specification.
+   * One can use nsRuleNode::GetRectValue to compute an nsCSSRect from
+   * the returned nsCSSValue.
+   */
+  bool ParseMarginString(const nsSubstring& aBuffer,
+                         nsIURI*            aURL,
+                         uint32_t           aLineNumber,
+                         nsCSSValue&        aValue,
+                         bool               aSuppressErrors = false);
+
+  /**
    * Parse aBuffer into a selector list.  On success, caller must
    * delete *aSelectorList when done with it.
    */
   nsresult ParseSelectorString(const nsSubstring&  aSelectorString,
                                nsIURI*             aURL,
                                uint32_t            aLineNumber,
                                nsCSSSelectorList** aSelectorList);
 
--- a/layout/style/nsCSSPropertyID.h
+++ b/layout/style/nsCSSPropertyID.h
@@ -58,17 +58,20 @@ enum nsCSSPropertyID {
   eCSSPropertyExtra_no_properties,
   eCSSPropertyExtra_all_properties,
 
   // Extra dummy values for nsCSSParser internal use.
   eCSSPropertyExtra_x_none_value,
   eCSSPropertyExtra_x_auto_value,
 
   // Extra value to represent custom properties (--*).
-  eCSSPropertyExtra_variable
+  eCSSPropertyExtra_variable,
+
+  // Extra value for use in the DOM API's
+  eCSSProperty_DOM
 };
 
 namespace mozilla {
 
 template<>
 inline PLDHashNumber
 Hash<nsCSSPropertyID>(const nsCSSPropertyID& aValue)
 {
--- a/layout/style/nsCSSValue.cpp
+++ b/layout/style/nsCSSValue.cpp
@@ -1213,17 +1213,18 @@ nsCSSValue::AppendAlignJustifyValueToStr
 
 void
 nsCSSValue::AppendToString(nsCSSPropertyID aProperty, nsAString& aResult,
                            Serialization aSerialization) const
 {
   // eCSSProperty_UNKNOWN gets used for some recursive calls below.
   MOZ_ASSERT((0 <= aProperty &&
               aProperty <= eCSSProperty_COUNT_no_shorthands) ||
-             aProperty == eCSSProperty_UNKNOWN,
+             aProperty == eCSSProperty_UNKNOWN ||
+             aProperty == eCSSProperty_DOM,
              "property ID out of range");
 
   nsCSSUnit unit = GetUnit();
   if (unit == eCSSUnit_Null) {
     return;
   }
 
   if (eCSSUnit_String <= unit && unit <= eCSSUnit_Attr) {
@@ -2424,17 +2425,18 @@ nsCSSRect::AppendToString(nsCSSPropertyI
   MOZ_ASSERT(mTop.GetUnit() != eCSSUnit_Null &&
              mTop.GetUnit() != eCSSUnit_Inherit &&
              mTop.GetUnit() != eCSSUnit_Initial &&
              mTop.GetUnit() != eCSSUnit_Unset,
              "parser should have used a bare value");
 
   if (eCSSProperty_border_image_slice == aProperty ||
       eCSSProperty_border_image_width == aProperty ||
-      eCSSProperty_border_image_outset == aProperty) {
+      eCSSProperty_border_image_outset == aProperty ||
+      eCSSProperty_DOM == aProperty) {
     NS_NAMED_LITERAL_STRING(space, " ");
 
     mTop.AppendToString(aProperty, aResult, aSerialization);
     aResult.Append(space);
     mRight.AppendToString(aProperty, aResult, aSerialization);
     aResult.Append(space);
     mBottom.AppendToString(aProperty, aResult, aSerialization);
     aResult.Append(space);
--- a/layout/style/nsCSSValue.h
+++ b/layout/style/nsCSSValue.h
@@ -660,16 +660,20 @@ public:
     { return eCSSUnit_EM <= mUnit && mUnit <= eCSSUnit_RootEM; }
   /**
    * A "pixel" length unit is a some multiple of CSS pixels.
    */
   static bool IsPixelLengthUnit(nsCSSUnit aUnit)
     { return eCSSUnit_Point <= aUnit && aUnit <= eCSSUnit_Pixel; }
   bool      IsPixelLengthUnit() const
     { return IsPixelLengthUnit(mUnit); }
+  static bool IsPercentLengthUnit(nsCSSUnit aUnit)
+    { return aUnit == eCSSUnit_Percent; }
+  bool      IsPercentLengthUnit()
+    { return IsPercentLengthUnit(mUnit); }
   static bool IsFloatUnit(nsCSSUnit aUnit)
     { return eCSSUnit_Number <= aUnit; }
   bool      IsAngularUnit() const  
     { return eCSSUnit_Degree <= mUnit && mUnit <= eCSSUnit_Turn; }
   bool      IsFrequencyUnit() const  
     { return eCSSUnit_Hertz <= mUnit && mUnit <= eCSSUnit_Kilohertz; }
   bool      IsTimeUnit() const  
     { return eCSSUnit_Seconds <= mUnit && mUnit <= eCSSUnit_Milliseconds; }