-
Notifications
You must be signed in to change notification settings - Fork 515
Add IntersectionObserver polyfill #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
} | ||
|
||
this._callback = callback; | ||
this._root = options.root || null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If 'root' is passed in, there should probably be some check that it's valid, and not an object or function or something random.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All observe()
calls will fail if root
is invalid. So this check is not strictly needed, but I agree it’s probably a good idea to add that check.
Should this polyfill throw an exception or otherwise indicate an error if someone tries to use it in a cross-domain iframe with |
this._observationTargets = new Map(); | ||
this._boundUpdate = this._update.bind(this); | ||
this.root.addEventListener('scroll', this._boundUpdate); | ||
this._intervalId = window.setInterval(this._boundUpdate, 100); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems less efficient than typical lazyload libraries, in that it polls the state of the observed element on every scroll event, and every 100ms. I would like to put this polyfill into the polyfill.io service library but am concerned about the frequency of these events. How about throttling the callback to one invocation per 100ms?
Also I assume we're polling separately to the scroll events to capture scenarios where the element moves from outside to inside the viewport without the document scroll position changing. Could this lean on MutationObserver to avoid setting up a tight interval timer that runs indefinitely?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point, "every scroll event" should probably be throttled to 100ms too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I agree that the polling and the scroll-events overlap. I can do something about that.
However, I am not sure about rate-limiting the updates to 100ms in general. If you want to use this data in animation, you ideally get an update from the IntersectionObserver every frame, right? Or did I miss something in the spec about this? I was thinking to use requestAnimationFrame()
so you get frame-by-frame updates in the polyfill.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm it would be great if the library can collect events as often as necessary, but batch delivering them and cap invoking the callback to no more than once every 100ms. Unfortunately, calling getBoundingClientRect on every scroll event is expensive. Maybe someone with more knowledge about the performance implications can weigh in? @KenjiBaheux @ojanvafai ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getBoundingClientRect()
is expensive, but that’s the polyfill only. The actual browser implementation will be more efficient and will probably dispatch on every frame, right? Otherwise the entire thing becomes rather useless for 60fps animations. Also, I thought you would use the thresholds to limit the number of events.
It’s a tough call and I’d appreciate more feedback. Personally, I’d rather emulate the same behavior of an actual browser implementation than worry too much about performance. It is a polyfill afterall. For more performance input, lemme loop in @paullewis.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The native implementation calculates intersections every frame, but it uses an idle callback with a timeout of 100ms to deliver them. To match that, you would need to separate the code that calculates the intersection from the code that delivers the notifications.
I agree that calculating intersections in a scroll handler is problematic from a performance perspective. It may very well trigger additional layouts, which is really anathema to whole purpose of this feature.
Even with the native implementation, notifications may be delayed by up to 100ms, so code that relies on up-to-the-frame information (e.g., input event handlers that want to validate the position of the clicked element before taking action) will need to use the takeRecords() method to get the most current information. With that in mind, my suggestion is this:
Do the intersection calculations on a 100ms timer. If takeRecords() is invoked, and too much time has passed since the last intersection calculation, then do the intersection calculation right away and return the up-to-date information to takeRecords().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the actual implementation fires on every rendered frame, basically. It can do this because it knows layout is clean at the time it runs, and doesn't have to worry about getBoundingClientRect triggering a layout.
The polyfill is closer to the actual implementation if it fires on every scroll and buffers, and triggers a callback based on a timer. But it will have performance implications.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not know that rIC will be used for dispatch. Agreed then. I will adjust the code accordingly.
Review has been implemented. PTAL :) |
I'm still concerned at the 100ms We're also building on I've started porting your tests into the polyfill.io library, and will take the polyfill once we've concluded this discussion. Can you declare the licence terms under which you are publishing this code? For polyfill.io we prefer MIT or CC0 but Apache (which appears to be one of the licences used in this repo) is fine too. |
You are totally right. MutationObserver are a way better approach than |
In polyfill.io we care about IE 6+ currently (yes really), but I take a
pragmatic view of it. It seems like falling back to element width and
height is easy, so why not. No need to do any heroics to get support in old
browsers but this seems easy enough that you may as well do it.
|
Hats off to you for supporting IE6 😄 As you suggested, I moved the implementation to use MutationObserver which makes this only work in IE11 and above, so there’s no point adding that fallback, or am I missing something? |
Well, polyfill.io polyfills for MutationObserver, and we can make the IO polyfill depend on MO, then in browsers that don't have MO we ship that polyfill as well! Also, we can offer some useful functionality here without MO, because many sites will only require the detection on scroll, so if you can shield the MO instantiation in a feature-detect, we can offer the polyfill on a wider range of browsers. |
Fair enough. I’ll add the fallback dimensions :) |
Done :) |
return; | ||
} | ||
var context = this, args = arguments; | ||
clearTimeout(timer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't timer
always be null
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not on rapid calls on the function being returned in line 213.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, nvm, you are right.
this._descheduleCallback(); | ||
this._callback(this._queue, this); | ||
this._queue = []; | ||
}.bind(this), {timeout: 100}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Magic number, move to constant?
@surma I am stuck in an airport for 6 hours so I did a more thorough code review. Hope that's OK - and some of these are points you may well disagree with. |
@triblondon Thank you very much for the thorough feedback. It’s highly appreciated :) Updated the code accordingly. |
Looks good. I'm working on getting the tests ported then I can test it in the polyfill service. |
if (timer) { | ||
return; | ||
} | ||
callback.apply(this, arguments); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
callback
=> fn
(sorry, this was wrong in my suggested code)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m so lazy... fix’d :D
OK, I ported the tests and pushed them here: If you install and run the Pretty good results in FF 38 and Chrome 49 using the polyfill, though it does not appear to support margins: Not so good in Chrome 49 with experimental web platform flag enabled, testing the native support: |
The problem with the native impl seems to be that records do not seem to be available synchronously with takeRecords(). Maybe. What do you think? |
Ah, the polyfill has a TODO against margin support, so I guess that my port of the tests is working then :-) So it's mostly a case of why the native impl fails the tests. |
Thanks a lot for your work, @triblondon. I’ll loop in @ojanvafai to check if my tests are wrong or the native implementation of Chrome is. |
For the record: Talked to @ojanvafai and rest of the team. I am making assumptions in the tests that are not correct and break with the native implementation. I’ll fix that. @triblondon You might have to re-port some of the tests. |
Would you consider using mocha/expect? It seems to make for much neater tests, and you should be able to run the tests I made without the overhead of the polyfill service. Just grab the Mocha demo, add Expect.js from CDNJS and replace the test cases with the contents of my file. |
Will do. |
Tests have been moved to mocha and I also made most of the tests properly asynchronous as Some checks are commented out as the native implementation in Chrome 51 is missing the Other than that, all tests pass on both native and the polyfill implementation except for the margin tests. Margin tests fail on both, ironically, even on native. Looking into that. |
@surma Maybe it will help you to refer to the root margin test for the native chromium implementation: |
@RByers why close? |
Whoops, sorry - that must have been a side-effect of switching to the gh-pages branch (#120) sorry. You'll probably need to update the PR to be based on gh-pages instead of the (now-deleted) master branch. |
Continued in #121 |
r: @ojanvafai
cc: @ianvollick @paullewis @flackr
This adds a polyfill for the IntersectionObserver API. It is compatible to the current version of the spec but ignores margins for now, as I wanted to avoid implementing a CSS string parser.
There’s also a simple test suite by @ojanvafai. All tests pass except for the margin tests for the reasons mentioned above. Please take a look at the tests to check that the polyfill is behaving as you’d expect.

Any other feedback obviously welcome as well :)