├── CONTRIBUTING.md ├── EventListenerOptions.polyfill.js ├── LICENSE.md ├── README.md ├── explainer.md └── w3c.json /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Web Platform Incubator Community Group 2 | 3 | This repository is being used for work in the Web Platform Incubator Community Group, governed by the [W3C Community License 4 | Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To contribute, you must join 5 | the CG. 6 | 7 | If you are not the sole contributor to a contribution (pull request), please identify all 8 | contributors in the pull request's body or in subsequent comments. 9 | 10 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows: 11 | 12 | ``` 13 | +@github_username 14 | ``` 15 | 16 | If you added a contributor by mistake, you can remove them in a comment with: 17 | 18 | ``` 19 | -@github_username 20 | ``` 21 | 22 | If you are making a pull request on behalf of someone else but you had no part in designing the 23 | feature, you can remove yourself with the above syntax. 24 | -------------------------------------------------------------------------------- /EventListenerOptions.polyfill.js: -------------------------------------------------------------------------------- 1 | // ==ClosureCompiler== 2 | // @compilation_level SIMPLE_OPTIMIZATIONS 3 | // @output_file_name EventListenerOptions.shim.min.js 4 | // @language ECMASCRIPT5 5 | // ==/ClosureCompiler== 6 | 7 | (function() { 8 | var supportsPassive = false; 9 | document.createElement("div").addEventListener("test", function() {}, { 10 | get passive() { 11 | supportsPassive = true; 12 | return false; 13 | } 14 | }); 15 | 16 | if (!supportsPassive) { 17 | var super_add_event_listener = EventTarget.prototype.addEventListener; 18 | var super_remove_event_listener = EventTarget.prototype.removeEventListener; 19 | var super_prevent_default = Event.prototype.preventDefault; 20 | 21 | function parseOptions(type, listener, options, action) { 22 | var needsWrapping = false; 23 | var useCapture = false; 24 | var passive = false; 25 | var fieldId; 26 | if (options) { 27 | if (typeof(options) === 'object') { 28 | passive = options.passive ? true : false; 29 | useCapture = options.useCapture ? true : false; 30 | } else { 31 | useCapture = options; 32 | } 33 | } 34 | if (passive) 35 | needsWrapping = true; 36 | if (needsWrapping) { 37 | fieldId = useCapture.toString(); 38 | fieldId += passive.toString(); 39 | } 40 | action(needsWrapping, fieldId, useCapture, passive); 41 | } 42 | 43 | Event.prototype.preventDefault = function() { 44 | if (this.__passive) { 45 | console.warn("Ignored attempt to preventDefault an event from a passive listener"); 46 | return; 47 | } 48 | super_prevent_default.apply(this); 49 | } 50 | 51 | EventTarget.prototype.addEventListener = function(type, listener, options) { 52 | var super_this = this; 53 | parseOptions(type, listener, options, 54 | function(needsWrapping, fieldId, useCapture, passive) { 55 | if (needsWrapping) { 56 | var fieldId = useCapture.toString(); 57 | fieldId += passive.toString(); 58 | 59 | if (!this.__event_listeners_options) 60 | this.__event_listeners_options = {}; 61 | if (!this.__event_listeners_options[type]) 62 | this.__event_listeners_options[type] = {}; 63 | if (!this.__event_listeners_options[type][listener]) 64 | this.__event_listeners_options[type][listener] = []; 65 | if (this.__event_listeners_options[type][listener][fieldId]) 66 | return; 67 | var wrapped = { 68 | handleEvent: function (e) { 69 | e.__passive = passive; 70 | if (typeof(listener) === 'function') { 71 | listener(e); 72 | } else { 73 | listener.handleEvent(e); 74 | } 75 | e.__passive = false; 76 | } 77 | }; 78 | this.__event_listeners_options[type][listener][fieldId] = wrapped; 79 | super_add_event_listener.call(super_this, type, wrapped, useCapture); 80 | } else { 81 | super_add_event_listener.call(super_this, type, listener, useCapture); 82 | } 83 | }); 84 | } 85 | 86 | EventTarget.prototype.removeEventListener = function(type, listener, options) { 87 | var super_this = this; 88 | parseOptions(type, listener, options, 89 | function(needsWrapping, fieldId, useCapture, passive) { 90 | if (needsWrapping && 91 | this.__event_listeners_options && 92 | this.__event_listeners_options[type] && 93 | this.__event_listeners_options[type][listener] && 94 | this.__event_listeners_options[type][listener][fieldId]) { 95 | super_remove_event_listener.call(super_this, type, this.__event_listeners_options[type][listener][fieldId], false); 96 | delete this.__event_listeners_options[type][listener][fieldId]; 97 | if (this.__event_listeners_options[type][listener].length == 0) 98 | delete this.__event_listeners_options[type][listener]; 99 | } else { 100 | super_remove_event_listener.call(super_this, type, listener, useCapture); 101 | } 102 | }); 103 | } 104 | } 105 | })(); 106 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All Reports in this Repository are licensed by Contributors under the 2 | [W3C Software and Document 3 | License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). Contributions to 4 | Specifications are made under the [W3C CLA](https://www.w3.org/community/about/agreements/cla/). 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Passive Event Listeners (EventListenerOptions) 2 | 3 | This work is now part of the [official WHATWG DOM spec](https://github.com/whatwg/dom). Please file any issues/pull requests there. This repository (and its resources / discussions) are only preserved here as an archive. 4 | 5 | --- 6 | 7 | An [extension](https://dom.spec.whatwg.org/#dictdef-eventlisteneroptions) to the DOM event pattern to allow listeners to disable support for `preventDefault`, primarily to enable scroll performance optimizations. See the [**explainer document**](https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md) for an overview. 8 | 9 | #### Spec changes 10 | * See the main [commit in the DOM specification](https://github.com/whatwg/dom/commit/253a21b8e78e37447c47983916a7cf39c4f6a3c5) or [pull request](https://github.com/whatwg/dom/pull/82) for full details. 11 | * The key parts of the spec affected by this are [EventTarget](https://dom.spec.whatwg.org/#eventtarget), [Observing event listeners](https://dom.spec.whatwg.org/#observing-event-listeners), and [preventDefault](https://dom.spec.whatwg.org/#dom-event-preventdefault) 12 | * Touch Events has [been updated](http://w3c.github.io/touch-events/#cancelability) to describe the performance optimization. 13 | * [Interventions issue 18](https://github.com/WICG/interventions/issues/18) tracks the plan to "[intervene](bit.ly/user-agent-intervention)" and force touch listeners to be passive in scenarios where the compat risk is low but the perf benefit large. 14 | 15 | #### Status of implementations: 16 | * See [CanIUse entry for passive event listeners](http://caniuse.com/#feat=passive-event-listener) 17 | * Chromium: [shipping](https://www.chromestatus.com/features/5745543795965952) in Chrome 51 ([launch bug](https://bugs.chromium.org/p/chromium/issues/detail?id=489802)) 18 | * [Chromium bug](https://bugs.chromium.org/p/chromium/issues/detail?id=599609) tracking the [next step](https://github.com/WICG/interventions/issues/18) of automatically opting some listeners into passive behavior. 19 | * WebKit: [EventListenerOptions support](https://bugs.webkit.org/show_bug.cgi?id=149466) and [performance optimization for touch listeners](https://bugs.webkit.org/show_bug.cgi?id=158601) landed. 20 | * [Performance optimization of wheel listeners](https://bugs.webkit.org/show_bug.cgi?id=158439) not yet implemented. 21 | * Firefox: [Landed](https://bugzilla.mozilla.org/show_bug.cgi?id=1266066) - planned to [ship in Firefox 49](https://platform-status.mozilla.org/#passive-event-listeners) 22 | * [DOM4 Polyfill](https://github.com/WebReflection/dom4), or sample [polyfill here](EventListenerOptions.polyfill.js) 23 | 24 | #### Additional background on the problem: 25 | * [Summary from Google I/O 2016 Mobile Web talk](https://youtu.be/0SSI8liELJU?t=6m20s) 26 | * [Ilya Grigorik's talk at Chrome Dev Summit](https://www.youtube.com/watch?v=NrEjkflqPxQ&feature=youtu.be&t=557) [[slides](https://docs.google.com/presentation/d/1WdMyLpuI93TR_w0fvKqFlUGPcLk3A4UJ2sBuUkeFcwU/present?slide=id.g7299ef155_0_7)] 27 | * [Discussion on twitter](https://twitter.com/RickByers/status/719736672523407360) with [demo video](https://www.youtube.com/watch?v=NPM6172J22g) 28 | * [Demo page with latency measurement](https://rbyers.github.io/scroll-latency.html) 29 | * Older [G+ post by Rick Byers](https://plus.google.com/+RickByers/posts/cmzrtyBYPQc) 30 | 31 | #### Additional resources for understaning and using passive listeners 32 | * [Tips for debugging scroll jank screencast](https://www.youtube.com/watch?v=6-D_3yx_KVI) 33 | 34 | #### Issues with and adoption by key libraries: 35 | * [Feature detect in Modernizr](https://github.com/Modernizr/Modernizr/blob/master/feature-detects/dom/passiveeventlisteners.js) ([issue](https://github.com/Modernizr/Modernizr/issues/1894)) 36 | * [Parse.ly](https://github.com/Parsely/time-engaged/issues/3) 37 | * [jQuery](https://github.com/jquery/jquery/issues/2871) 38 | * [Ember.js](https://github.com/emberjs/ember.js/issues/12783) 39 | 40 | #### History: 41 | * [Outstanding issues](https://github.com/WICG/EventListenerOptions/issues?q=is%3Aissue) 42 | * [WICG discussion](https://discourse.wicg.io/t/eventlisteneroptions-and-passive-event-listeners-move-to-wicg/1386/13) 43 | * [Discussion on WhatWG](https://lists.w3.org/Archives/Public/public-whatwg-archive/2015Jul/0018.html) 44 | * One [discussion on public-pointer-events](https://lists.w3.org/Archives/Public/public-pointer-events/2015AprJun/0042.html) 45 | * Earlier [scroll-blocks-on proposal](https://docs.google.com/document/d/1aOQRw76C0enLBd0mCG_-IM6bso7DxXwvqTiRWgNdTn8/edit#heading=h.wi06xpj70hhd) and discussion 46 | -------------------------------------------------------------------------------- /explainer.md: -------------------------------------------------------------------------------- 1 | # Passive event listeners 2 | 3 | Passive event listeners are a new feature [in the DOM spec](https://dom.spec.whatwg.org/#dom-addeventlisteneroptions-passive) that enable developers to opt-in to better scroll performance by eliminating the need for scrolling to block on touch and wheel event listeners. Developers can annotate touch and wheel listeners with `{passive: true}` to indicate that they will never invoke `preventDefault`. This feature [shipped in Chrome 51](https://www.chromestatus.com/features/5745543795965952), [Firefox 49](https://bugzilla.mozilla.org/show_bug.cgi?id=1266066) and [landed in WebKit](https://bugs.webkit.org/show_bug.cgi?id=158601). Check out the video below for a side-by-side of passive event listeners in action: 4 | 5 | [demo video](https://www.youtube.com/watch?v=NPM6172J22g) 6 | 7 | ## The problem 8 | 9 | Smooth scrolling performance is essential to a good experience on the web, especially on touch-based devices. 10 | All modern browsers have a threaded scrolling feature to permit scrolling to run smoothly even when expensive 11 | JavaScript is running, but this optimization is partially defeated by the need to wait for the results of 12 | any `touchstart` and `touchmove` handlers, which may prevent the scroll entirely by calling [`preventDefault()`](http://www.w3.org/TR/touch-events/#the-touchstart-event) on the event. While there are particular scenarios where an author may indeed want to prevent scrolling, analysis indicates that the majority of touch event handlers on the web never actually 13 | call `preventDefault()`, so browsers often block scrolling unnecessarily. For instance, in Chrome for Android 80% of the touch events that block scrolling never actually prevent it. 10% of these events add more than 100ms of delay to the start of scrolling, and a catastrophic delay of at least 500ms occurs in 1% of scrolls. 14 | 15 | Many developers are surprised to learn that [simply adding an empty touch handler to their document](http://rbyers.github.io/janky-touch-scroll.html) can have a 16 | significant negative impact on scroll performance. Developers quite reasonably expect that the act of observing an event [should not have any side-effects](https://dom.spec.whatwg.org/#observing-event-listeners). 17 | 18 | The fundamental problem here is not limited to touch events. [`wheel` events](https://w3c.github.io/uievents/#events-wheelevents) 19 | suffer from an identical issue. In contrast, [pointer event handlers](https://w3c.github.io/pointerevents/) are 20 | designed to never delay scrolling (though developers can declaratively suppress scrolling altogether with the `touch-action` CSS property), so do not suffer from this issue. Essentially the passive event listener proposal brings the performance properties of pointer events to touch and wheel events. 21 | 22 | This proposal provides a way for authors to indicate at handler registration time whether the handler may call `preventDefault()` on the event (i.e. whether it needs an event that is [cancelable](https://dom.spec.whatwg.org/#dom-event-cancelable)). When no touch or wheel handlers at a particular point require a cancelable event, a user agent is free to start scrolling immediately without waiting for JavaScript. That is, passive listeners are free from surprising performance side-effects. 23 | 24 | ## EventListenerOptions 25 | 26 | First, we need a mechanism for attaching additional information to an event listener. Today the `capture` argument to `addEventListener` is the closest example of something like this, but its usage is pretty opaque: 27 | 28 | ```javascript 29 | document.addEventListener('touchstart', handler, true); 30 | ``` 31 | 32 | [`EventListenerOptions`](https://dom.spec.whatwg.org/#dictdef-eventlisteneroptions) lets us write this more explicitly as: 33 | 34 | ```javascript 35 | document.addEventListener('touchstart', handler, {capture: true}); 36 | ``` 37 | 38 | This is simply the new (extensible) syntax for existing behavior - specifying [whether you want the listener invoked during the capture phase or bubbling phase](http://javascript.info/tutorial/bubbling-and-capturing#capturing). 39 | 40 | ## Solution: the 'passive' option 41 | 42 | Now that we have an extensible syntax for specifying options at event handler registration time, we can add a new `passive` option which declares up-front that the listener will never call `preventDefault()` on the event. If it does, the user agent will just ignore the request (ideally generating at least a console warning), as it already does for events with `Event.cancelable=false`. A developer can verify this by querying `Event.defaultPrevented` before and after calling `preventDefault()`. Eg: 43 | 44 | ```javascript 45 | addEventListener(document, "touchstart", function(e) { 46 | console.log(e.defaultPrevented); // will be false 47 | e.preventDefault(); // does nothing since the listener is passive 48 | console.log(e.defaultPrevented); // still false 49 | }, Modernizr.passiveeventlisteners ? {passive: true} : false); 50 | ``` 51 | 52 | Now rather than having to block scrolling whenever there are any touch or wheel listener, the browser only needs to do this when there are *non-passive* listeners (see [TouchEvents spec](http://w3c.github.io/touch-events/#cancelability)). `passive` listeners are free of performance side-effects. 53 | 54 | **By marking a touch or wheel listener as `passive`, the developer is promising the handler won't call `preventDefault` to disable scrolling.** This frees the browser up to respond to scrolling immediately without waiting for JavaScript, thus ensuring a reliably smooth scrolling experience for the user. 55 | 56 | ## Feature Detection 57 | 58 | Because older browsers will interpret any object in the 3rd argument as a `true` value for the `capture` argument, it's important for developers to use feature detection or [a polyfill](https://github.com/WebReflection/dom4) when using this API, to avoid unforeseen results. Feature detection for specific options can be done as follows: 59 | 60 | ```javascript 61 | // Test via a getter in the options object to see if the passive property is accessed 62 | var supportsPassive = false; 63 | try { 64 | var opts = Object.defineProperty({}, 'passive', { 65 | get: function() { 66 | supportsPassive = true; 67 | } 68 | }); 69 | window.addEventListener("testPassive", null, opts); 70 | window.removeEventListener("testPassive", null, opts); 71 | } catch (e) {} 72 | 73 | // Use our detect's results. passive applied if supported, capture will be false either way. 74 | elem.addEventListener('touchstart', fn, supportsPassive ? { passive: true } : false); 75 | ``` 76 | 77 | To make this simpler you can use the feature detect from [Detect It](https://github.com/rafrex/detect-it), eg: 78 | ```javascript 79 | elem.addEventListener('touchstart', fn, 80 | detectIt.passiveEvents ? {passive:true} : false); 81 | ``` 82 | 83 | [Modernizr](https://modernizr.com/) also has a detect [here](https://github.com/Modernizr/Modernizr/pull/1982). There is an [open standards debate](https://github.com/heycam/webidl/issues/107) around providing a simpler API for dictionary member feature detection. 84 | 85 | 86 | ## Removing the need to cancel events 87 | 88 | There are scenarios where an author may intentionally want to consistently disable scrolling by cancelling all touch or wheel events. These include: 89 | 90 | * Panning and zooming a map 91 | * Full-page/full-screen games 92 | 93 | In these cases, the current behavior (which prevents scrolling optimization) is perfectly adequate, since scrolling itself is being prevented consistently. There is no need to use passive listeners in these cases, though it's often still a good idea to apply a `touch-action: none` CSS rule to make your intention explicit (eg. supporting browsers with Pointer Events but not Touch Events). 94 | 95 | However, in a number of common scenarios events don't need to block scrolling - for instance: 96 | 97 | * User activity monitoring which just wants to know when the user was last active 98 | * `touchstart` handlers that hide some active UI (like tooltips) 99 | * `touchstart` and `touchend` handlers that style UI elements (without suppressing the `click` event). 100 | 101 | For these scenarios, the `passive` option can be added (with appropriate feature detection) without any other code changes, resulting in a significantly smoother scrolling experience. 102 | 103 | There are a few more complicated scenarios where the handler only wants to suppress scrolling under certain conditions, such as: 104 | 105 | * Swiping horizontally to rotate a carousel, dismiss an item or reveal a drawer, while still permitting vertical scrolling. 106 | * In this case, use [touch-action: pan-y](https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action) to declaratively disable scrolling that starts along the horizontal axis without having to call `preventDefault()` ([test page](https://rbyers.github.io/touch-action.html)). 107 | * To continue to work correctly in all browsers, calls to `preventDefault` should be conditional on the lack of support for the particular `touch-action` rule being used (note that Safari 9 currently only supports `touch-action: manipulation`). 108 | * A UI element (like YouTube's volume slider) which slides on horizontal wheel events without changing the scrolling behavior on vertical wheel events. Since there is no equivalent of "touch-action" for wheel events, this case can only be implemented with non-passive wheel listeners. 109 | * Event delegation patterns where the code that adds the listener won't know if the consumer will cancel the event. 110 | * One option here is to do delegation separately for passive and non-passive listeners (as if they were different event types entirely). 111 | * It's also possible to leverage `touch-action` as above (treating Touch Events as you would [Pointer Events](https://w3c.github.io/pointerevents/). 112 | 113 | ## Debugging and measuring the benefit 114 | 115 | You can get a quick idea of the benefit possible (and potential breakage) by forcing touch/wheel listeners to be treated as passive via chrome://flags/#passive-listener-default (new in Chrome 52). This makes it easy to do your own side-by-side comparisons like [this popular video](https://twitter.com/RickByers/status/719736672523407360). 116 | 117 | See [this video](https://www.youtube.com/watch?v=6-D_3yx_KVI) for tips on how to use Chrome's Developer Tools to identify listeners that are blocking scrolling. You can [monitor event timestamps](http://rbyers.net/scroll-latency.html) to measure scroll jank in the wild, and use [Chromium's tracing system](https://www.chromium.org/developers/how-tos/trace-event-profiling-tool) to look at the InputLatency records for scrolling when debugging. 118 | 119 | The Chrome team is working on a proposal for both a [PerformanceTimeline API](https://code.google.com/p/chromium/issues/detail?id=543598) and more [DevTools features](https://code.google.com/p/chromium/issues/detail?id=520659) to help web developers get better visibility into this problem today. 120 | 121 | ## Reducing and breaking up long-running JS is still critical 122 | 123 | When a page exhibits substantial scroll jank, it's always an indication of an underlying peformance issue somewhere. Passive event listeners do nothing to address these underlying issues, so we still strongly encourage developers to ensure that their application meets the [RAIL guidelines](https://developers.google.com/web/tools/chrome-devtools/profile/evaluate-performance/rail?hl=en) even on low-end devices. If your site has logic that runs for >100ms at a time, it will still feel sluggish in response to taps / clicks. Passive event listeners just allow developers to decouple the issue of having JS responsiveness reflected in scroll performance from the desire to monitor input events. In particular, developers of third-party analytics libraries can now have some confidence that their use of light-weight event listeners will not fundamentally change the observed performance characteristics of any page using their code. 124 | 125 | ## Further reading and discussion 126 | 127 | See the links [here](https://github.com/WICG/EventListenerOptions) for more details. For questions or concerns, feel free to [file issues on this repo](https://github.com/WICG/EventListenerOptions/issues), or reach out to [@RickByers](https://twitter.com/RickByers/). 128 | -------------------------------------------------------------------------------- /w3c.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": ["80485"] 3 | , "contacts": ["yoavweiss"] 4 | , "shortName": "EventListenerOptions" 5 | } --------------------------------------------------------------------------------