├── .pr-preview.json ├── loaf-explainer.md ├── w3c.json ├── LICENSE.md ├── CODE_OF_CONDUCT.md ├── raf.css ├── style.css ├── .github └── workflows │ ├── build.yml │ └── auto-publish.yml ├── Makefile ├── demo-child.html ├── render-jank-demo.html ├── demo.html ├── CONTRIBUTING.md ├── init.js ├── README.md └── index.bs /.pr-preview.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_file": "index.bs", 3 | "type": "bikeshed" 4 | } 5 | -------------------------------------------------------------------------------- /loaf-explainer.md: -------------------------------------------------------------------------------- 1 | # Long Animation Frames (LoAF) 2 | 3 | Please see https://github.com/w3c/long-animation-frames 4 | -------------------------------------------------------------------------------- /w3c.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": 45211, 3 | "contacts": [ 4 | "carine" 5 | ], 6 | "shortName": "longtasks", 7 | "repo-type": "rec-track" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All documents in this Repository are licensed by contributors 2 | under the 3 | [W3C Software and Document License](http://www.w3.org/Consortium/Legal/copyright-software). 4 | 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/). 4 | -------------------------------------------------------------------------------- /raf.css: -------------------------------------------------------------------------------- 1 | div { 2 | width: 7px; 3 | height:7px; 4 | background: green; 5 | float: left; 6 | } 7 | 8 | button { 9 | position: absolute; 10 | top: 70px; 11 | left: 20px; 12 | } 13 | #stop { 14 | left: 100px; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | button { 2 | font-size: 15px; 3 | } 4 | iframe { 5 | padding: 20px; 6 | border: 20px solid rebeccapurple; 7 | } 8 | .eventLog { 9 | width: 90%; 10 | height: 200px; 11 | border: 1px solid; 12 | overflow: auto; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Build 16 | run: make ci 17 | - name: Deploy 18 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 19 | uses: peaceiris/actions-gh-pages@v3 20 | with: 21 | github_token: ${{ secrets.GITHUB_TOKEN }} 22 | publish_dir: ./out 23 | 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | 3 | local: index.bs 4 | bikeshed --die-on=warning spec index.bs index.html 5 | 6 | index.html: index.bs 7 | @ (HTTP_STATUS=$$(curl https://api.csswg.org/bikeshed/ \ 8 | --output index.html \ 9 | --write-out "%{http_code}" \ 10 | --header "Accept: text/plain, text/html" \ 11 | -F die-on=warning \ 12 | -F file=@index.bs) && \ 13 | [[ "$$HTTP_STATUS" -eq "200" ]]) || ( \ 14 | echo ""; cat index.html; echo ""; \ 15 | rm -f index.html; \ 16 | exit 22 \ 17 | ); 18 | 19 | remote: index.html 20 | 21 | ci: index.bs 22 | mkdir -p out 23 | make remote 24 | mv index.html out/index.html 25 | 26 | clean: 27 | rm index.html 28 | 29 | 30 | -------------------------------------------------------------------------------- /demo-child.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | child frame A 6 | 7 | 8 | 9 | 10 | 11 | 12 |

child iframe

13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/auto-publish.yml: -------------------------------------------------------------------------------- 1 | # Configuration options https://w3c.github.io/spec-prod/ 2 | name: Node CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: {} 9 | 10 | jobs: 11 | validate-and-publish: 12 | name: Validate and Publish 13 | runs-on: ubuntu-latest # only linux supported at present 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: w3c/spec-prod@v2 17 | with: 18 | TOOLCHAIN: bikeshed 19 | W3C_ECHIDNA_TOKEN: ${{ secrets.ECHIDNA_TOKEN }} 20 | W3C_WG_DECISION_URL: "https://lists.w3.org/Archives/Public/public-web-perf/2021Apr/0005.html" 21 | W3C_NOTIFICATIONS_CC: "${{ secrets.CC }}" 22 | W3C_BUILD_OVERRIDE: | 23 | status: WD 24 | TR: https://www.w3.org/TR/longtasks-1/ 25 | -------------------------------------------------------------------------------- /render-jank-demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Long Task: Render & Jank Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Render & Jank

16 | 17 | 18 |

19 | 20 | 21 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Long Task Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributions to this repository are intended to become part of Recommendation-track documents governed by the 2 | [W3C Patent Policy](https://www.w3.org/Consortium/Patent-Policy-20040205/) and 3 | [Software and Document License](https://www.w3.org/Consortium/Legal/copyright-software). To make substantive contributions to specifications, you must either participate 4 | in the relevant W3C Working Group or make a non-member patent licensing commitment. 5 | 6 | For our editing test-driven process, see [CONTRIBUTING.md](https://github.com/w3c/web-performance/blob/gh-pages/CONTRIBUTING.md). 7 | 8 | If you are not the sole contributor to a contribution (pull request), please identify all 9 | contributors in the pull request comment. 10 | 11 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows: 12 | 13 | ``` 14 | +@github_username 15 | ``` 16 | 17 | If you added a contributor by mistake, you can remove them in a comment with: 18 | 19 | ``` 20 | -@github_username 21 | ``` 22 | 23 | If you are making a pull request on behalf of someone else but you had no part in designing the 24 | feature, you can remove yourself with the above syntax. 25 | -------------------------------------------------------------------------------- /init.js: -------------------------------------------------------------------------------- 1 | function makeSlowTask(ms) { 2 | var begin = window.performance.now(); 3 | while (window.performance.now() < begin + ms); 4 | } 5 | 6 | 7 | (function loop() { 8 | // Random number in range 100 - 400ms 9 | var randTaskLen = Math.round(Math.random() * (400 - 10)) + 10; 10 | var randDelay = Math.round(Math.random() * (1000 - 300)) + 300; 11 | setTimeout(function() { 12 | makeSlowTask(randTaskLen); 13 | loop(); 14 | }, randDelay); 15 | }()); 16 | 17 | var globalID; 18 | 19 | function repeatOften() { 20 | $("
").appendTo("body"); 21 | globalID = requestAnimationFrame(repeatOften); 22 | } 23 | 24 | globalID = requestAnimationFrame(repeatOften); 25 | 26 | function stopAnimating() { 27 | cancelAnimationFrame(globalID); 28 | } 29 | 30 | function addJank() { 31 | var begin = window.performance.now(); 32 | while (window.performance.now() < begin + 450); 33 | }; 34 | 35 | function init() { 36 | var p = document.getElementById("jank"); 37 | p.onclick = addJank; 38 | 39 | var q = document.getElementById("stop"); 40 | if (q) { 41 | q.onclick = stopAnimating; 42 | } 43 | 44 | console.log('Make observer'); 45 | window._observer = new PerformanceObserver(function(entryList) { 46 | var entries = entryList.getEntries(); 47 | for (var i = 0; i < entries.length; i++) { 48 | var newItem = "long task! " + "start: " + entries[i].startTime + ", duration: " + (entries[i].duration) + "ms, name: " + entries[i].name; 49 | var logBox = document.getElementById('eventlog'); 50 | if (logBox && entries[i].entryType == "longtask") { 51 | logBox.innerHTML = newItem + "
" + logBox.innerHTML; 52 | } 53 | } 54 | }); 55 | console.log('Observe longtask'); 56 | window._observer.observe({entryTypes: ["longtask"]}); 57 | }; 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Long Task API 2 | 3 | Long Tasks is a new real user measurement (RUM) performance API to enable applications to measure responsiveness. It enables detecting presence of “long tasks” that monopolize the UI thread for extended periods of time and block other critical tasks from being executed - e.g. reacting to user input. 4 | 5 | ## Background 6 | As the page is loading and while the user is interacting with the page afterwards, both the application and browser queue various events that are then executed by the browser -- e.g. the user agent schedules input events based on user’s activity, the application schedules callbacks for requestAnimationFrame and other callbacks etc. Once in the queue, these events are then dequeued one-by-one by the browser and executed — see [“the anatomy of a frame”](https://aerotwist.com/blog/the-anatomy-of-a-frame) for a high-level overview of this process in Blink. 7 | 8 | However, some tasks can take a long time (multiple frames), and if and when that happens, the UI thread is locked and all other tasks are blocked as well. To the user this is commonly visible as a “locked up” page where the browser is unable to respond to user input; this is a major source of bad user experience on the web today: 9 | 10 | * _Delayed [“time to Interactive”](https://github.com/tdresser/time-to-interactive)_: while the page is loading long tasks often tie up the main thread and prevent the user from interacting with the page even though the page is visually rendered. Poorly designed third-party content is a frequent culprit. 11 | * _High/variable input latency_: critical user interaction events (tap, click, scroll, wheel, etc) are queued behind long tasks, which yields janky and unpredictable user experience. 12 | * _High/variable event handling latency_: similar to input, but for processing event callbacks (e.g. onload events, and so on), which delay application updates. 13 | * _Janky animations and scrolling_: some animation and scrolling interactions require coordination between compositor and main threads; if the main thread is blocked due to a long task, it can affect responsiveness of animations and scrolling. 14 | 15 | Some applications (and RUM vendors) are already attempting to identify and track cases where “long tasks” happen. For example, one known pattern is to install a ~short periodic timer and inspect the elapsed time between the successive calls: if the elapsed time is greater than the timer period, then there is high likelihood that one or more long tasks have delayed execution of the timer. This mostly works, but it has several bad performance implications: the application is polling to detect long tasks, which prevents quiescence and long idle blocks (see requestIdleCallback); it’s bad for battery life; and there is no way to know what caused the delay. (e.g. first party vs third party code) 16 | 17 | [RAIL performance model](https://developers.google.com/web/tools/chrome-devtools/profile/evaluate-performance/rail?hl=en#response-respond-in-under-100ms) suggests that applications should respond in under 100ms to user input; for touch move and scrolling in under 16ms. Our goal with this API is to surface notifications about tasks that may prevent the application from hitting these targets. 18 | 19 | ## Terminology 20 | Major terms: 21 | * **frame** or **frame context** refers to the browsing context, such as iframe (not animation frame), embed or object 22 | * **culprit frame** refers to the frame or container (iframe, object, embed etc) that is being implicated for the long task 23 | * **attribution** refers to identifying the type of work (such as script, layout etc.) that contributed significantly to the long task AND which browsing context or frame is responsible for that work. 24 | * **minimal frame attribution** refers to the browsing context or frame that is being implicated overall for the long task 25 | 26 | ## V1 API 27 | Long Task API introduces a new PerformanceEntry object, which will report instances of long tasks: 28 | ```javascript 29 | interface PerformanceLongTaskTiming : PerformanceEntry { 30 | [SameObject, SaveSameObject] readonly attribute FrozenArray attribution; 31 | }; 32 | ``` 33 | 34 | Attribute definitions of PerformanceLongTaskTiming: 35 | * entryType: "longtask" 36 | * startTime: `DOMHighResTimeStamp` of when long task started 37 | * duration: elapsed time (as `DOMHighResTimeStamp`) between start and finish of task 38 | * name: minimal frame attribution, eg. "same-origin", "cross-origin", "unknown" etc. Possible values are: 39 | * "self" 40 | * "same-origin-ancestor" 41 | * "same-origin-descendant" 42 | * "same-origin" 43 | * "cross-origin-ancestor" 44 | * "cross-origin-descendant" 45 | * "cross-origin-unreachable" 46 | * "multiple-contexts" 47 | * "unknown" 48 | 49 | * attribution: `sequence` of `TaskAttributionTiming`, a new `PerformanceEntry` object to report attribution within long tasks. To see how `attribute` is populated for different values of `name` see the section below: [Pointing to the culprit](#pointing-to-the-culprit) 50 | 51 | ```javascript 52 | interface TaskAttributionTiming : PerformanceEntry { 53 | readonly attribute DOMString containerType; 54 | readonly attribute DOMString containerSrc; 55 | readonly attribute DOMString containerId; 56 | readonly attribute DOMString containerName; 57 | }; 58 | ``` 59 | 60 | Attribute definitions of TaskAttributionTiming: 61 | * entryType: “taskattribution” 62 | * startTime: 0 63 | * duration: 0 64 | * name: type of attribution, eg. "script" or "layout" 65 | * containerType: type of container for culprit frame eg. "iframe" (most common), "embed", "object". 66 | * containerName: `DOMString`, container’s name attribute 67 | * containerId: `DOMString`, container’s id attribute 68 | * containerSrc: `DOMString`, container’s src attribute 69 | 70 | 71 | Long tasks events will be delivered to all observers (in frames within the page or tab) regardless of which frame was responsible for the long task. The goal is to allow all pages on the web to know if and who (first party content or third party content) is causing disruptions. 72 | 73 | The `name` field provides minimal frame attribution so that the observing frame can respond to the issue in the proper way. In addition, the `attribution` field provides further insight into the type of work (script, layout etc) that caused the long task as well as which frame is responsible for that work. For more details on how the `attribution` is set, see the "Pointing to the culprit" section. 74 | 75 | The above covers existing use cases found in the wild, enables document-level attribution, and eliminates the negative performance implications mentioned earlier. To receive these notifications, the application can subscribe to them via PerformanceObserver interface: 76 | 77 | ```javascript 78 | const observer = new PerformanceObserver(function(list) { 79 | for (const entry of list.getEntries()) { 80 | // Process long task notifications: 81 | // report back for analytics and monitoring 82 | // ... 83 | } 84 | }); 85 | 86 | 87 | // Register observer for long task notifications. 88 | // Since the "buffered" flag is set, longtasks that already occurred are received. 89 | observer.observe({type: "longtask", buffered: true}); 90 | 91 | // Long script execution after this will result in queueing 92 | // and receiving “longtask” entries in the observer. 93 | ``` 94 | 95 | **Long-task threshold is 50ms.** That is, the UA should emit long-task events whenever it detects tasks whose execution time exceeds >50ms. 96 | 97 | ### Demo 98 | For a quick demo, visit this [website](https://longtasks.glitch.me/render-jank-demo.html) on a browser which supports the Long Tasks API. 99 | 100 | For a demo of long tasks from same-origin & cross-origin frames, see this [website](https://longtasks.glitch.me/demo.html). 101 | Interacting with the iframed wikipedia page will generate cross-origin long task notifications. 102 | 103 | ### Pointing to the culprit 104 | Long task represents the top level event loop task. Within this task, different types of work (such as script, layout, style etc) may be done, and they could be executed within different frame contexts. The type of work could also be global in nature such as a long GC that is process or frame-tree wide. 105 | 106 | Thus pointing to the culprit has couple of facets: 107 | * Pointing to the overall frame to blame for the long task on the whole: this is refered to as "minimal frame attribution" and is captured in the `name` field 108 | * Pointing to the type of work involved in the task, and its associated frame context: this is captured in `TaskAttributionTiming` objects in the `attribution` field of `PerformanceLongTaskTiming` 109 | 110 | Therefore, `name` and `attribution` fields on PerformanceLongTaskTiming together paint the picture for where the blame rests for a long task. 111 | 112 | The security model of the web means that sometimes a long task will happen in an iframe that is unreachable from the observing frame. For instance, a long task might happen in a deeply nested iframe that is different from my origin. Or similarly, I might be an iframe doubly embedded in a document, and a long task will happen in the top-level browsing context. In the web security model, I can know from which direction the issue came, one of my ancestors or descendants, but to preserve the frame origin model, we must be careful about pointing to the specific container or frame. 113 | 114 | Currently the TaskAttributionTiming entry in `attribution` is populated with "script" work (in the future layout, style etc will be added). The container or frame implicated in `attribution` should match up with the `name` as follows: 115 | 116 | | value of `name` | frame implicated in `attribution`| 117 | | ----------------------- |:-------------------------:| 118 | | self | empty | 119 | | same-origin-ancestor | same-origin culprit frame | 120 | | same-origin-descendant | same-origin culprit frame | 121 | | same-origin | same-origin culprit frame | 122 | | cross-origin-ancestor | empty | 123 | | cross-origin-descendant | first cross-origin child frame between my own frame and culprit frame| 124 | | cross-origin-unreachable| empty | 125 | | multiple-contexts | empty | 126 | | unknown | empty | 127 | 128 | 129 | ## Privacy & Security 130 | Long Tasks API surfaces long tasks greater than a threshold (50ms) to developers via Javascript (Performance Observer API). It includes origin-safe attribution information about the source of the long task. 131 | There is a 50ms threshold for long tasks. Together this provides adequate protection against security attacks against browser. 132 | 133 | However, privacy related attacks are possible, while the API doesn’t introduce any new privacy attacks, it could expedite existing privacy attacks. If this were to become an concern, additional mitigations can be implemented to address this such as dropping "culprit" after a per-target origin threshold is exceeded, or limiting to 10 origins per minute etc. 134 | 135 | Detailed Security & Privacy doc is here: 136 | https://docs.google.com/document/d/1tIMI1gau_q6X5EBnjDNiFS5NWV9cpYJ5KKA7xPd3VB8/edit# 137 | 138 | ## V2 API Sketch 139 | See: https://docs.google.com/document/d/125d69JAC7nyx-Ob0a9Z31d1uHUGu4myYQ3os9EnGfdU/edit 140 | 141 | ## Alternatives Considered 142 | ### Why not just show sub-tasks vs. top-level tasks with attribution? 143 | This API will show toplevel long tasks along with attribution for specific sub-tasks which were problematic. 144 | For instance, within a 50ms toplevel task, sub-tasks such as a 20ms script execution or a 30ms style & layout update -- will be attributed. 145 | This raises the question -- why show the toplevel task at all? Why not only show long sub-tasks such as script, style & layout etc that are directly actionable by the user? The top level task may contain some un-attributable segments such as browser work eg. GC or browser events etc. 146 | 147 | The rationale here is that showing the toplevel task is good for web developers, even though they will actively consume the actionable sub-tasks such as long scripts and act on them. Over time the sub-task attribution will keep expanding, making more of the long task actionable. 148 | Showing the top-level task gives developers a direct indication of main thread busy-ness, and since this directly impacts the user experience, it is appropriate for them to know about it as a problem signal -- even if they cannot have complete visibility or full actionability for the entire length of the long task. 149 | In many cases the developers may be able to repro in lab or locally and glean additional insights and get to the root cause. 150 | Long tasks provide context to long sub-tasks, for instance, a 20ms style and layout or a 25ms script execution may not be terrible by themselves, but if they happen consecutively (eg. script started from rAF) and cause a long 50ms task, then this is a problem for user responsiveness. 151 | 152 | -------------------------------------------------------------------------------- /index.bs: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | 25 |
 26 | urlPrefix: https://w3c.github.io/performance-timeline/; spec: PERFORMANCE-TIMELINE-2;
 27 |     type: interface; url: #the-performanceentry-interface; text: PerformanceEntry;
 28 |     type: attribute; for: PerformanceEntry;
 29 |         text: name; url: #dom-performanceentry-name
 30 |         text: entryType; url: #dom-performanceentry-entrytype
 31 |         text: startTime; url: #dom-performanceentry-starttime
 32 |         text: duration; url: #dom-performanceentry-duration
 33 |     type: dfn; url: #dfn-queue-a-performanceentry; text: Queue the PerformanceEntry
 34 |     type: dfn; url: #dfn-register-a-performance-entry-type; text: register a performance entry type
 35 |     type: attribute; for: PerformanceObserver;
 36 |         text: supportedEntryTypes; url: #supportedentrytypes-attribute;
 37 | urlPrefix: https://w3c.github.io/hr-time/; spec: HR-TIME-2;
 38 |     type: typedef; url: #idl-def-domhighrestimestamp; text: DOMHighResTimeStamp;
 39 |     type: interface; url: #dfn-performance; text: Performance;
 40 |     type: attribute; for: Performance;
 41 |         text: now(); url: #dom-performance-now
 42 |     type: dfn; text: current high resolution time; url: #dfn-current-high-resolution-time;
 43 | urlPrefix: https://html.spec.whatwg.org/multipage/; spec: HTML;
 44 |     type: dfn; url: #event-loop; text: event loop;
 45 |     type: dfn; url: #event-loop-processing-model; text: event loop processing model;
 46 |     type: dfn; url: #browsing-context; text: browsing context;
 47 |     type: dfn; url: #calling-scripts; text: calling scripts;
 48 |     type: dfn; url: #list-of-the-descendant-browsing-contexts; text: list of the descendant browsing contexts;
 49 |     type: dfn; url: #ancestor-browsing-context; text: ancestor;
 50 |     type: dfn; url: #browsing-context-group-set; text: browsing context group set
 51 |     type: dfn; url: #script-evaluation-environment-settings-object-set; text: script evaluation environment settings object set
 52 |     type: dfn; url: #integration-with-the-javascript-agent-cluster-formalism; text: agent cluster
 53 |     type: dfn; url: #concept-task-document; for: task; text: document;
 54 |     type: dfn; url: #running-script; text: running script;
 55 |     type: dfn; url: #muted-errors; for: classic script; text: muted errors;
 56 |     type: dfn; url: #cors-cross-origin; text: CORS cross-origin;
 57 | urlPrefix: https://tc39.github.io/ecma262/; spec: ECMASCRIPT;
 58 |     type: dfn; url: #sec-code-realms; text: JavaScript Realms;
 59 | urlPrefix: https://dom.spec.whatwg.org/; spec: DOM;
 60 |     type: attribute; for: Element;
 61 |         text: id; url: #dom-element-id;
 62 | urlPrefix: https://webidl.spec.whatwg.org/; spec: WEBIDL;
 63 |     type: dfn; text: identifier; url: #dfn-identifier;
 64 |     type: dfn; text: attribute; url: #dfn-attribute;
 65 | urlPrefix: https://tc39.es/ecma262/multipage/managing-memory.html
 66 |     type: dfn; text: weakrefderef; url: #sec-weakrefderef;
 67 | 
68 | 69 | 72 | 73 | Introduction {#intro} 74 | ===================== 75 | 76 | As the page is loading and while the user is interacting with the page afterwards, both the application and browser queue various events that are then executed by the browser -- e.g. user agent schedules input events based on user’s activity, the application schedules callbacks for requestAnimationFrame and other callbacks, etc. Once in the queue, the browser dequeues these events one-by-one and executes them. 77 | 78 | However, some tasks can take a long time (multiple frames) and if/when that happens, the UI thread may become blocked and block all other tasks as well. To the user, this is commonly visible as a "locked up" page where the browser is unable to respond to user input; this is a major source of bad user experience on the web today: 79 | 80 | : Delayed "time to Interactive": 81 | :: while the page is loading, or even completely visually rendered, long tasks often tie up the main thread and prevent the user from interacting with the page. Poorly designed third-party content is frequently the culprit. 82 | 83 | : High/variable input latency: 84 | :: critical user-interaction events (e.g. tap, click, scroll, wheel, etc.) are queued behind long tasks which yields janky and unpredictable user experience. 85 | 86 | : High/variable event handling latency: 87 | :: like input, processing event callbacks (e.g. onload events, etc.) delay application updates. 88 | 89 | : Janky animations and scrolling: 90 | :: some animation and scrolling interactions require coordination between compositor and main threads; if a long task is blocking the main thread it can affect responsiveness of animations and scrolling. 91 | 92 | Some applications (and RUM vendors) are already attempting to identify and track cases where "long tasks" happen. For example, one known pattern is to install a ~short periodic timer and inspect the elapsed time between the successive expirations: if the elapsed time is greater than the timer period, then there is high likelihood that one or more long tasks have delayed execution of the event loop. This approach mostly works but has several bad performance implications: by polling to detect long tasks, the application prevents quiescence and long idle blocks (see requestIdleCallback); it’s bad for battery life; there is no way to know what is causing the delay (e.g. first party or third party code). 93 | 94 | The RAIL performance model suggests that applications should respond to user input in less than 100ms (for touch move and scrolling, the threshold is 16ms). The goal of this API is to surface notifications about tasks that may prevent the application from hitting these targets. This API surfaces tasks that take 50ms or more. A website without these tasks should respond to user input in under 100ms: it will take less than 50ms to finish the task that is being executed when the user input is received and less than 50ms to execute the task to react to such user input. 95 | 96 | Usage Example {#example} 97 | ------------------------ 98 | 99 |
100 |     const observer = new PerformanceObserver(function(list) {
101 |         for (const entry of list.getEntries()) {
102 |             // Process long task notifications:
103 |             // report back for analytics and monitoring
104 |             // ...
105 |         }
106 |     });
107 |     // Register observer for previous and future long task notifications.
108 |     observer.observe({type: "longtask", buffered: true});
109 |     // Long script execution after this will result in queueing
110 |     // and receiving "longtask" entries in the observer.
111 | 
112 | 113 | Terminology {#sec-terminology} 114 | ============================== 115 | 116 | Long task refers to any of the following occurrences whose duration exceeds 50ms: 117 | 118 | * An event loop task plus the perform a microtask checkpoint that follows immediately afterwards. This captures the duration of an event loop task, including its associated microtasks. 119 | 120 | * An update the rendering step within the event loop processing model. 121 | 122 | * A pause between the last step and the next first step of the event loop processing model. This captures any work that the user agent performs in its UI thread outside of the event loop. 123 | 124 | The browsing context container for a [=browsing context=] |bc| is |bc|'s [=navigable/active document=]'s [=node navigable=]'s [=navigable/container=]. 125 | 126 | Note: This term is outdated, and the new terms should be reused when revamping this. 127 | 128 | Culprit browsing context container refers to the browsing context container (<{iframe}>, <{object}>, etc.) that is being implicated, on the whole, for a long task. 129 | 130 | Attribution refers to identifying the type of work (such as script, layout etc.) that contributed significantly to the long task, as well as identifying which culprit browsing context container is responsible for that work. 131 | 132 | Long Task Timing {#sec-longtask-timing} 133 | ======================================= 134 | 135 | Long Task timing involves the following new interfaces: 136 | 137 | {{PerformanceLongTaskTiming}} interface {#sec-PerformanceLongTaskTiming} 138 | ------------------------------------------------------------------------ 139 | 140 |
141 |     [Exposed=Window]
142 |     interface PerformanceLongTaskTiming : PerformanceEntry {
143 |         /* Overloading PerformanceEntry */
144 |         readonly attribute DOMHighResTimeStamp startTime;
145 |         readonly attribute DOMHighResTimeStamp duration;
146 |         readonly attribute DOMString name;
147 |         readonly attribute DOMString entryType;
148 | 
149 |         readonly attribute FrozenArray<TaskAttributionTiming> attribution;
150 |         [Default] object toJSON();
151 |     };
152 | 
153 | 154 | The values of the attributes of a {{PerformanceLongTaskTiming}} are set in the processing model in [[#report-long-tasks]]. The following provides an informative summary of how they will be set. 155 | 156 | The {{PerformanceEntry/name}} attribute's getter will return one of the following strings: 157 | 158 | : "unknown" 159 | :: The long task originated from work that the user agent performed outside of the event loop. 160 | : "self" 161 | :: The long task originated from an event loop task within this browsing context. 162 | : "same-origin-ancestor" 163 | :: The long task originated from an event loop task within a same-origin ancestor navigable. 164 | : "same-origin-descendant" 165 | :: The long task originated from an event loop task within a same-origin descendant browsing context. 166 | : "same-origin" 167 | :: The long task originated from an event loop task within a same-origin browsing context that is not an ancestor or descendant. 168 | : "cross-origin-ancestor" 169 | :: The long task originated from an event loop task within a cross-origin ancestor navigable. 170 | : "cross-origin-descendant" 171 | :: The long task originated from an event loop task within a cross-origin descendant browsing context. 172 | : "cross-origin-unreachable" 173 | :: The long task originated from an event loop task within a cross-origin browsing context that is not an ancestor or descendant. 174 | : "multiple-contexts" 175 | :: The long task originated from an event loop task involving multiple browsing contexts. 176 | 177 | Note: There are some inconsistencies across these names, such as the "-unreachable" and the "-contexts" suffixes. 178 | These names are kept for backward compatibility reasons. 179 | 180 | The {{PerformanceLongTaskTiming/entryType}} attribute's getter step is to return "longtask". 181 | 182 | The {{PerformanceLongTaskTiming/startTime}} attribute's getter step is to return a {{DOMHighResTimeStamp}} of when the task started. 183 | 184 | The {{PerformanceLongTaskTiming/duration}} attribute's getter step is to return a {{DOMHighResTimeStamp}} equal to the elapsed time between the start and end of task, with a 1 ms granularity. 185 | 186 | The attribution attribute's getter will return a frozen array of {{TaskAttributionTiming}} entries. 187 | 188 | {{TaskAttributionTiming}} interface {#sec-TaskAttributionTiming} 189 | ---------------------------------------------------------------- 190 | 191 |
192 |     [Exposed=Window]
193 |     interface TaskAttributionTiming : PerformanceEntry {
194 |         /* Overloading PerformanceEntry */
195 |         readonly attribute DOMHighResTimeStamp startTime;
196 |         readonly attribute DOMHighResTimeStamp duration;
197 |         readonly attribute DOMString name;
198 |         readonly attribute DOMString entryType;
199 | 
200 |         readonly attribute DOMString containerType;
201 |         readonly attribute DOMString containerSrc;
202 |         readonly attribute DOMString containerId;
203 |         readonly attribute DOMString containerName;
204 |         [Default] object toJSON();
205 |     };
206 | 
207 | 208 | The values of the attributes of a {{TaskAttributionTiming}} are set in the processing model in [[#report-long-tasks]]. The following provides an informative summary of how they will be set. 209 | 210 | The {{TaskAttributionTiming/name}} attribute's getter will always return "unknown". 211 | 212 | The {{TaskAttributionTiming/entryType}} attribute's getter will always return "taskattribution". 213 | 214 | The {{TaskAttributionTiming/startTime}} attribute's getter will always return 0. 215 | 216 | The {{TaskAttributionTiming/duration}} attribute's getter will always return 0. 217 | 218 | The containerType attribute's getter will return the type of the culprit browsing context container, such as "iframe", "embed", or "object". If no single culprit browsing context container is found, it will return "window". 219 | 220 | The containerName attribute's getter will return the value of the container's name content attribute. If no single culprit browsing context container is found, it will return the empty string. 221 | 222 | The containerId attribute's getter will return the value of the container's id content attribute. If no single culprit browsing context container is found, it will return the empty string. 223 | 224 | The containerSrc attribute's getter will return the value of the container's src content attribute. If no single culprit browsing context container is found, it will return the empty string. 225 | 226 | Pointing to the culprit {#sec-PointingToCulprit} 227 | ------------------------------------------------ 228 | 229 |
230 | 231 | This section is non-normative. 232 | 233 | A long task can involve different types of work (such as script, layout, style etc), and it could be executed within different browsing contexts, or it could be global in nature such as a long garbage collection that spans the entire agent cluster or browsing context group set. 234 | 235 | Thus attribution has a couple of facets: 236 | 237 | * Pointing to the origin of the long task and/or the overall location of the culprit browsing context: this is referred to as minimal culprit attribution and is captured in the {{PerformanceEntry/name}} field. 238 | 239 | * Pointing to the type of work involved in the long task, and its associated culprit browsing context container: this is captured in {{TaskAttributionTiming}} objects in the {{PerformanceLongTaskTiming/attribution}} field of {{PerformanceLongTaskTiming}}. 240 | 241 | Therefore, {{PerformanceEntry/name}} and {{PerformanceLongTaskTiming/attribution}} fields on {{PerformanceLongTaskTiming}} together paint the picture for where the blame rests for a long task. 242 | When delivering this information the Web's same-origin policy must be adhered to. 243 | 244 | These fields are not independent. The following gives an overview of how they are related: 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 256 | 259 | 262 | 265 | 268 | 271 | 274 | 277 |
{{PerformanceEntry/name}}Culprit browsing context container implicated by {{PerformanceLongTaskTiming/attribution}}
"self" 254 | empty 255 |
"same-origin-ancestor" 257 | same-origin culprit 258 |
"same-origin-descendant" 260 | same-origin culprit 261 |
"same-origin" 263 | same-origin culprit 264 |
"cross-origin-ancestor" 266 | empty 267 |
"cross-origin-descendant" 269 | empty 270 |
"cross-origin-unreachable" 272 | empty 273 |
"multiple-contexts" 275 | empty 276 |
"unknown" 278 | empty 279 |
280 | 281 |
282 | 283 | Processing model {#sec-processing-model} 284 | ======================================== 285 | 286 | Note: A user agent implementing the Long Tasks API would need to include "longtask" 287 | in {{PerformanceObserver/supportedEntryTypes}} for {{Window}} contexts, respectively. 288 | 289 | This allows developers to detect support for long tasks. 290 | 291 | Report long tasks {#report-long-tasks} 292 | -------------------------------------------------------- 293 | 294 |
295 | Given |start time|, |end time|, |top-level browsing contexts|, and |task|, perform the following algorithm: 296 | 297 | 1. [=Record task end time=] given |end time| and |task|'s [=task/document=]. 298 | 299 | 1. If |end time| minus |start time| is less than the long tasks threshold of 50 ms, abort these steps. 300 | 301 | 1. Let |destinationRealms| be an empty set. 302 | 303 | 1. Determine the set of [=JavaScript Realms=] to which reports will be delivered: 304 | 305 | For each [=top-level browsing context=] |topmostBC| in |top-level browsing contexts|: 306 | 307 | 1. Add |topmostBC|'s [=active document=]'s [=relevant Realm=] to |destinationRealms|. 308 | 1. Let |descendantBCs| be |topmostBC|'s [=active document=]'s [=list of the descendant browsing contexts=]. 309 | 1. Let |document| be |descendantBC|'s [=active document=]. 310 | 1. For each |descendantBC| in |descendantBCs|, add (|document|'s [=relevant Realm=], |document|'s [=relevant settings object=]'s [=environment settings object/cross-origin isolated capability=]) to |destinationRealms|. 311 | 312 | 1. A user agent may remove some [=JavaScript Realms=] from |destinationRealms|. 313 | 314 | Note: this removal could be used to avoid reporting long tasks for [=JavaScript Realms=] that the user agent handles in a separate process. However, this concept is not specified precisely. 315 | 316 | Issue(75): there is some ongoing discussion regarding the scope of which {{Document|Documents}} gain visibility over which long tasks, so this logic could change in the future. 317 | 318 | 1. For each (|destinationRealm|, |crossOriginIsolatedCapability|) in |destinationRealms|: 319 | 320 | 1. Let |name| be the empty string. This will be used to report [=minimal culprit attribution=], below. 321 | 1. Let |culpritSettings| be null. 322 | 1. Process |task|'s [=script evaluation environment settings object set=] to determine |name| and |culpritSettings| as follows: 323 | 324 | 1. If |task|'s [=script evaluation environment settings object set=] is empty: set |name| to "[=unknown=]" and |culpritSettings| to null. 325 | 1. Otherwise, if |task|'s [=script evaluation environment settings object set=]'s length is greater than one: set |name| to "[=multiple-contexts=]" and |culpritSettings| to null. 326 | 1. Otherwise, i.e. if |task|'s [=script evaluation environment settings object set=]'s length is one: 327 | 1. Set |culpritSettings| to the single item in |task|'s [=script evaluation environment settings object set=]. 328 | 1. Let |destinationSettings| be |destinationRealm|'s [=relevant settings object=]. 329 | 1. Let |destinationOrigin| be |destinationSettings|'s [=environment settings object/origin=]. 330 | 1. Let |destinationBC| be |destinationSettings|'s [=environment settings object/global object=]'s [=Window/browsing context=]. 331 | 1. Let |culpritBC| be |culpritSettings|'s [=environment settings object/global object=]'s [=Window/browsing context=]. 332 | 1. Assert: |culpritBC| is not null. 333 | 1. If |culpritSettings| is the same as |destinationSettings|, set |name| to "[=self=]". 334 | 1. Otherwise, if |culpritSettings|'s [=environment settings object/origin=] and |destinationOrigin| are [=same origin=]: 335 | 1. If |destinationBC| is null, set |name| to "[=same-origin=]". 336 | 1. Otherwise, if |culpritBC| is an [=ancestor=] of |destinationBC|, set |name| to "[=same-origin-ancestor=]". 337 | 1. Otherwise, if |destinationBC| is an [=ancestor=] of |culpritBC|, set |name| to "[=same-origin-descendant=]". 338 | 1. Otherwise, set |name| to "[=same-origin=]". 339 | 1. Otherwise: 340 | 1. If |destinationBC| is null, set |name| to "[=cross-origin-unreachable=]". 341 | 1. Otherwise, if |culpritBC| is an [=ancestor=] of |destinationBC|, set |name| to "[=cross-origin-ancestor=]" and set |culpritSettings| to null. 342 | 343 | NOTE: this is not reported because of security. Developers should look this up themselves. 344 | 345 | 1. Otherwise, if |destinationBC| is an [=ancestor=] of |culpritBC|, set |name| to "[=cross-origin-descendant=]". 346 | 1. Otherwise, set |name| to "[=cross-origin-unreachable=]". 347 | 348 | 1. Let |attribution| be a new {{TaskAttributionTiming}} object with |destinationRealm| and set its attributes as follows: 349 | 1. Set |attribution|'s {{PerformanceEntry/name}} attribute to "[=unknown=]". 350 | 351 | NOTE: future iterations of this API will add more values to the {{PerformanceEntry/name}} attribute of a {{TaskAttributionTiming}} object, but for now it can only be a single value. 352 | 353 | 1. Set |attribution|'s {{PerformanceEntry/entryType}} attribute to "taskattribution". 354 | 1. Set |attribution|'s {{PerformanceEntry/startTime}} and {{PerformanceEntry/duration}} to 0. 355 | 1. Set |attribution|'s {{containerType}} attribute to "window". 356 | 1. Set |attribution|'s {{containerName}} and {{containerSrc}} attributes to the empty string. 357 | 1. If |culpritSettings| is not null: 358 | 1. Let |culpritBC| be |culpritSettings|'s [=environment settings object/global object=]'s [=Window/browsing context=]. 359 | 1. Assert: |culpritBC| is not null. 360 | 1. Let |container| be |culpritBC|'s [=browsing context container=]. 361 | 1. Assert: |container| is not null. 362 | 1. Set |attribution|'s {{containerId}} attribute to the value of |container|'s [=Element/ID=], or the empty string if the ID is unset. 363 | 1. If |container| is an <{iframe}> element: 364 | 1. Set |attribution|'s {{containerType}} attribute to "iframe". 365 | 1. Set |attribution|'s {{containerName}} attribute to the value of |container|'s <{iframe/name}> content attribute, or the empty string if the attribute is absent. 366 | 1. Set |attribution|'s {{containerSrc}} attribute to the value of |container|'s <{iframe/src}> content attribute, or the empty string if the attribute is absent. 367 | 368 | NOTE: it is intentional that we record the frame's <{iframe/src}> attribute here, and not its current URL, as this is meant primarily to help identify frames, and allowing discovery of the current URL of a cross-origin iframe is a security problem. 369 | 370 | 1. If |container| is a <{frame}> element: 371 | 1. Set |attribution|'s {{containerType}} attribute to "frame". 372 | 1. Set |attribution|'s {{containerName}} attribute to the value of |container|'s name content attribute, or the empty string if the attribute is absent. 373 | 1. Set |attribution|'s {{containerSrc}} attribute to the value of |container|'s src content attribute, or the empty string if the attribute is absent. 374 | 1. If |container| is an <{object}> element: 375 | 1. Set |attribution|'s {{containerType}} attribute to "object". 376 | 1. Set |attribution|'s {{containerName}} attribute to the value of |container|'s name content attribute, or the empty string if the attribute is absent. 377 | 1. Set |attribution|'s {{containerSrc}} attribute to the value of |container|'s <{object/data}> content attribute, or the empty string if the attribute is absent. 378 | 1. If |container| is an <{embed}> element: 379 | 1. Set |attribution|'s {{containerType}} attribute to "embed". 380 | 1. Set |attribution|'s {{containerName}} attribute to the empty string. 381 | 1. Set |attribution|'s {{containerSrc}} attribute to the value of |container|'s <{embed/src}> content attribute, or the empty string if the attribute is absent. 382 | 383 | 1. Create a new {{PerformanceLongTaskTiming}} object |newEntry| with |destinationRealm| and set its attributes as follows: 384 | 385 | 1. Set |newEntry|'s {{PerformanceEntry/name}} attribute to |name|. 386 | 1. Set |newEntry|'s {{PerformanceEntry/entryType}} attribute to "longtask". 387 | 1. Set |newEntry|'s {{PerformanceEntry/startTime}} attribute to the result of [=coarsen time|coarsening=] |start time| given |crossOriginIsolatedCapability|. 388 | 1. Let |dur| be the result of [=coarsen time|coarsening=] |end time| given |crossOriginIsolatedCapability|, minus |newEntry|'s {{PerformanceEntry/startTime}}. 389 | 1. Set |newEntry|'s {{PerformanceEntry/duration}} attribute to the integer part of |dur|. 390 | 1. If |attribution| is not null, set |newEntry|'s {{PerformanceLongTaskTiming/attribution}} attribute to a new frozen array containing the single value |attribution|. 391 | 392 | NOTE: future iterations of this API will add more values to the {{PerformanceLongTaskTiming/attribution}} attribute, but for now it only contains a single value. 393 | 394 | 1. [=Queue the PerformanceEntry=] |newEntry|. 395 |
396 | 397 | 398 | Security & privacy considerations {#priv-sec} 399 | =============================================== 400 | 401 | Long Tasks API adheres to the same-origin policy by including origin-safe attribution information about 402 | the source of the long task. There is a 50ms threshold for long tasks. Durations are only provided in 1 ms 403 | granularity. Together this provides adequate protection against cross-origin leaks. 404 | 405 | The Long Tasks API provides timing information about the duration and type of tasks executed by the user, 406 | as well as attribution such as the browsing context causing the function calls. This could enable an attacker 407 | to perform side-channel timing attacks to guess the user’s action, or identify the user. For example, a pattern of 408 | long script followed by a long render could be put together to guess user’s interaction with a social 409 | widget. Detailed function call attribution would be used to determine the user’s action. 410 | 411 | While the API doesn’t introduce any new privacy attacks, it could make existing privacy attacks faster. 412 | Mitigations for this are possible and can be implemented as needed: 413 | 414 | * Further clamp or add random jitter to the long task duration provided by the API to make attacks harder to 415 | exploit. 416 | * Limit the number of origins for which longtasks are exposed by the API, and obfuscate the attribution of 417 | any tasks afterwards. For instance, a page with 5 iframes could receive only attribution for tasks from 3 418 | of those iframes, and would receive no attribution ({{PerformanceEntry/name}} set to unknown") 419 | for tasks from the other 2. 420 | * Allow dropping the culprit/attribution information after a certain threshold. For instance, after 10 longtasks 421 | all entries would receive no attribution and their {{PerformanceEntry/name}} would be "unknown". 422 | * Add a built-in delay to the timing information exposed to make attacks dependent on longtask volume harder 423 | to execute. 424 | 425 | What is Exposed to Observers? {#what-is-exposed} 426 | -------------------------------------------------------- 427 | 428 | All observers within the top level page (i.e. all iframes in the page and the main frame) will receive 429 | notifications about presence of long tasks. We expose the start time of the task, its duration (with 1 ms 430 | granularity), and a pointer to the culprit frame. This information can already be observed today, and with 431 | higher resolution, using setTimeout. An attacker can do this by clearing everything else on the page and adding 432 | the vulnerable cross-origin resource to ensure that delays from the setTimeout are caused by that resource. 433 | Observers in other different pages (tabs or windows) should not receive notifications, regardless of the 434 | architecture of the user agent. 435 | 436 | Cross origin rules for what is exposed: 437 | * Cross-origin observers may see the direction of the culprit e.g if the culprit is a deeply nested iframe, 438 | then the host page can see the first cross-origin between itself and the culprit. 439 | * Conversely, if the culprit is the top level page, then a deeply embedded iframe can see that a longtask 440 | occurrred in its cross-origin ancestor but does not receive any information about it. 441 | 442 | Attack Scenarios Considered {#attack-scenarios} 443 | ----------------------------------------------- 444 | 445 | The following are the timing attacks considered: 446 | 447 | 1. Traditional timing attacks: using external resource load time to reveal the size of 448 | private data. For instance the number of hidden pictures in a gallery, whether username is 449 | valid, etc. See an example. 450 | 451 | 1. Side-channel timing attacks: using time for video parsing, script parsing, App Cache reads 452 | or Cache API (service workers) usage to uniquely identify a user, or to create a profile of the 453 | user’s age, gender, location, and interests etc. For 454 | instance, status updates from 455 | a social network can be limited to certain demographic (eg. females of age 20-30) the file size of 456 | the permalink page can be used to determine whether the user is in the target demographic. 457 | 458 | These scenarios are addressed by the 50ms threshold AND respecting cross-origin boundary i.e. not 459 | showing task type or additional attribution to untrusted cross origin observers. 460 | --------------------------------------------------------------------------------