LinkedIn's JavaScript viewport tracking library and IntersectionObserver polyfill. Track what the user actually sees.
import { InteractionObserver } from 'spaniel';
new InteractionObserver((entries) => { console.log('I see you') }, {
threshold: 0.5
}).observe(document.getElementById('my-element'));
Practical uses included:
- Determining advertisement impressions
- Impression discounting feedback for relevance systems
- Occlusion culling - Don't render an object until user is close to scrolling the object into the viewport
Spaniel provides APIs at multiple levels of abstraction for determining when DOM elements are visible, with the lowest level of abstraction being a polyfill for the IntersectionObserver API. Spaniel does not actually send viewport/impression tracking events, but rather should be setup to call your app code that does the actual sending of viewport/impression tracking events.
There are three different classes provided:
spaniel.IntersectionObserver
- A polyfill for IntersectionObserver.spaniel.SpanielObserver
- Same API structure as the IntersectionObserver, but with some added functionality for adding time thresholds in addition to ratio thresholds.spaniel.Watcher
- Provides an API for receiving high level events about the visibility state of DOM elements.
The main part of the IntersectionObserver API not supported by Spaniel's pollyfill is IntersectionObserver.root
A superset API of InteractionObserver. There are three additions:
Instead of threshold
being an array of ratios, threshold
is an array of objects with the following structure:
let options = {
threshold: [{
label: 'impressed',
ratio: 0.5,
time: 1000
}]
}
The example threshold is only considered crossed with the intersection ratio
has been met for the specified time
in milliseconds. When the threshold is crossed, the resulting entry passed to the observer callback will include the label
.
The observer callback is passed an array of change entries. In addition to the standard entry fields from IntersectionObserver
, the following properties have been added:
duration
- How long the elements intersection ratio was above the threshold before going below the threshold. Only passed for change entries associated with going below the threshold.entering
- Boolean describing if the intersection ratio is changing to be above the threshold ratio.label
- Which threshold is being crossedpayload
- Optional payload object provided byobserve()
observe()
can take an optional second parameter, an object that is included in the change entry passed to the Observer callback.
import { SpanielObserver } from 'spaniel';
let observer = new SpanielObserver((entries) => {
let entry = entries[0];
if (entry.entering) {
console.log(`${entry.payload.name} has been at least 50% visible for one second`);
} else {
console.log(`${entry.payload.name} was at least 50% visible for ${entry.duration} milliseconds`);
}
}, {
threshold: [{
label: 'impressed',
ratio: 0.5,
time: 1000
}]
});
let target = document.getElementById('my-element');
observer.observe(target, {
name: 'My Element'
});
The Watcher
API is similar in structure, but simpler compared to the IntersectionObserver
. Watcher
is streamlined for practical scenarios.
import { Watcher } from 'spaniel';
let watcher = new Watcher({
time: 100,
ratio: 0.8
});
let target = document.getElementById('my-element');
watcher.watch(target, (eventName, meta) => {
console.log(`My element was ${eventName} for ${meta.duration} milliseconds`);
});
watcher.unwatch(myOtherTarget);
There are four different event types passed to the watcher callback:
exposed
- When at least one pixel of the DOM element is in the viewportvisible
- When the configured percentage of the DOM element is in the viewport, or when the DOM element takes up the configured percentage of the screen.impressed
- When the DOM element has been visible for the configured amount of time.impression-complete
- When an impressed element is no longer impressed. This event includes the total duration of time the element was visible.
If no config is passed to the Watcher
constructor, only the exposed
event is fired.
The Watcher
constructor can be passed 3 different options:
time
- The time thresholdratio
- The ratio thresholdrootMargin
- The rootMargin in object form.
Under the hood, Spaniel uses requestAnmiationFrame
to preform microtask scheduling. Spaniel does not use mutation observers, scroll listeners, or resize listeners. Instead, requestAnmiationFrame
polling is used for performance reasons.
Spaniel exposes an API for hooking into the built-in requestAnmiationFrame
task scheduling engine, or even setting your own requestAnmiationFrame
task scheduling engine.
import { on, off, scheduleRead, scheduleWork } from 'spaniel';
// Do something on scroll
on('scroll', (frame) => {
console.log('I scrolled to ' + frame.scrollTop);
});
function onResize(frame) {
console.log('Viewport is ' + frame.width + 'px wide');
}
// Do something on window resize
on('resize', onResize);
// Stop watching resize
off('resize', onResize);
// Triggered after all spaniel observer callbacks have fired during `unload`
on('destroy', flushBeacons);
// Proxy to visibilitychange API
on('show', onTabFocus);
on('hide', onTabUnfocus);
scheduleRead(() => {
console.log('This will get executed during the DOM read phase of the rAF loop');
});
scheduleWork(() => {
console.log('This will get executed during the write/mutation phase of the rAF loop');
});
With any task engine involving the DOM, DOM reads and DOM writes should be batched seperately. For this reason, it's important that any work that forces a browser layout be scheduled via scheduleRead()
, while any work that modifies the layout should be scheduled via scheduleWork()
.
If you'd like to use an exisiting requestAnmiationFrame polling/task engine, use setGlobalEngine(engine)
, where engine
is an object that implements the EngineInterface
. As an example, you can checkout Spaniel's internal Engine implementation.
- Provides the future-proofing of a WCIG API, but with an expanded feature-set built upon said API.
- Tested and iterated upon in production by LinkedIn since late 2014
- Highly performant, only relies on
requestAnmiationFrame
- Extensive
requestAnmiationFrame
task/utility API
Spaniel has both unit tests and a headless test suite. The headless tests are run using Nightmare.
The minified UMD file is 3.71 kB gzipped.
You can also run npm run stats
to measure locally.
Spaniel is a standard NPM/CommonJS module. You can use a build tool like browserify or webpack to include Spaniel in your application.
If you're using rollup, an ES6 version is built at /exports/es6/spaniel.js
(as noted by jsnext:main
in package.json
).
Alternatively, running npm run build will generate a UMD file at /exports/spaniel.js
, and a minified UMD file at /exports/min/spaniel.js
. You can use the minified file in production.
The Spaniel source code is written in TypeScript.
// Install dependencies
npm install
// Run build
npm run build
// Watch and auto-rebuild
npm run watch
// Serve test app
npm run serve
// Run the tests
npm run test
- https://github.com/WICG/IntersectionObserver
- https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
Copyright 2016 LinkedIn Corp. All rights reserved.