1 # Performance best practices for Firefox front-end engineers
3 This guide will help Firefox developers working on front-end code
4 produce code which is as performant as possible—not just on its own, but
5 in terms of its impact on other parts of Firefox. Always keep in mind
6 the side effects your changes may have, from blocking other tasks, to
7 interfering with other user interface elements.
9 ## Avoid the main thread where possible
11 The main thread is where we process user events and do painting. It's
12 also important to note that most of our JavaScript runs on the main
13 thread, so it's easy for script to cause delays in event processing or
14 painting. That means that the more code we can get off of the main
15 thread, the more that thread can respond to user events, paint, and
16 generally be responsive to the user.
18 You might want to consider using a Worker if you need to do some
19 computation that can be done off of the main thread. If you need more
20 elevated privileges than a standard worker allows, consider using a
21 ChromeWorker, which is a Firefox-only API which lets you create
22 workers with more elevated privileges.
24 ## Use requestIdleCallback()
26 If you simply cannot avoid doing some kind of long job on the main
27 thread, try to break it up into smaller pieces that you can run when the
28 browser has a free moment to spare, and the user isn't doing anything.
29 You can do that using **requestIdleCallback()** and the [Cooperative
30 Scheduling of Background Tasks API](https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API),
31 and doing it only when we have a free second where presumably the user
32 isn’t doing something.
34 See also the blog post [Collective scheduling with requestIdleCallback](https://hacks.mozilla.org/2016/11/cooperative-scheduling-with-requestidlecallback/).
36 As of [bug 1353206](https://bugzilla.mozilla.org/show_bug.cgi?id=1353206),
37 you can also schedule idle events in non-DOM contexts by using
38 **Services.tm.idleDispatchToMainThread**. See the
39 **nsIThreadManager.idl** file for more details.
43 If you’re adding a new XUL *\<xul:popup\>* or *\<xul:panel\>* to a
44 document, set the **hidden** attribute to **true** by default. By doing
45 so, you cause the binding applied on demand rather than at load time,
46 which makes initial construction of the XUL document faster.
48 ## Get familiar with the pipeline that gets pixels to the screen
50 Learn how pixels you draw make their way to the screen. Knowing the path
51 they will take through the various layers of the browser engine will
52 help you optimize your code to avoid pitfalls.
54 The rendering process goes through the following steps:
56 ![This is the pipeline that a browser uses to get pixels to the screen](img/rendering.png)
58 The above image is used under [Creative Commons Attribution 3.0](https://creativecommons.org/licenses/by/3.0/),
59 courtesy of [this page](https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing)
60 from our friends at Google, which itself is well worth the read.
62 For a very down-to-earth explanation of the Style, Layout, Paint and
63 Composite steps of the pipeline, [this Hacks blog post](https://hacks.mozilla.org/2017/08/inside-a-super-fast-css-engine-quantum-css-aka-stylo/)
64 does a great job of explaining it.
66 To achieve a 60 FPS frame rate, all of the above has to happen in 16
67 milliseconds or less, every frame.
69 Note that **requestAnimationFrame()** lets you queue up JavaScript to
70 **run right before the style flush occurs**. This allows you to put all
71 of your DOM writes (most importantly, anything that could change the
72 size or position of things in the DOM) just before the style and layout
73 steps of the pipeline, combining all the style and layout calculations
74 into a single batch so it all happens once, in a single frame tick,
75 instead of across multiple frames.
77 See [Detecting and avoiding synchronous reflow](#detecting-and-avoiding-synchronous-reflow)
78 below for more information.
80 This also means that *requestAnimationFrame()* is **not a good place**
81 to put queries for layout or style information.
83 ## Detecting and avoiding synchronous style flushes
85 ### What are style flushes?
86 When CSS is applied to a document (HTML or XUL, it doesn’t matter), the
87 browser does calculations to figure out which CSS styles will apply to
88 each element. This happens the first time the page loads and the CSS is
89 initially applied, but can happen again if JavaScript modifies the DOM.
91 JavaScript code might, for example, change DOM node attributes (either
92 directly or by adding or removing classes from elements), and can also
93 add, remove, or delete DOM nodes. Because styles are normally scoped to
94 the entire document, the cost of doing these style calculations is
95 proportional to the number of DOM nodes in the document (and the number
96 of styles being applied).
98 It is expected that over time, script will update the DOM, requiring us
99 to recalculate styles. Normally, the changes to the DOM just result in
100 the standard style calculation occurring immediately after the
101 JavaScript has finished running during the 16ms window, inside the
102 "Style" step. That's the ideal scenario.
104 However, it's possible for script to do things that force multiple style
105 calculations (or **style flushes**) to occur synchronously during the
106 JavaScript part of the 16 ms window. The more of them there are, the
107 more likely they'll exceed the 16ms frame budget. If that happens, some
108 of them will be postponed until the next frame (or possibly multiple
109 frames, if necessary), this skipping of frames is called **jank**.
111 Generally speaking, you force a synchronous style flush any time you
112 query for style information after the DOM has changed within the same
113 frame tick. Depending on whether or not [the style information you’re
114 asking for has something to do with size or position](https://gist.github.com/paulirish/5d52fb081b3570c81e3a)
115 you may also cause a layout recalculation (also referred to as *layout
116 flush* or *reflow*), which is also an expensive step see [Detecting
117 and avoiding synchronous reflow](#detecting-and-avoiding-synchronous-reflow) below.
119 To avoid this: avoid reading style information if you can. If you *must*
120 read style information, do so at the very beginning of the frame, before
121 any changes have been made to the DOM since the last time a style flush
124 Historically, there hasn't been an easy way of doing this - however,
125 [bug 1434376](https://bugzilla.mozilla.org/show_bug.cgi?id=1434376)
126 has landed some ChromeOnly helpers to the window binding to
129 If you want to queue up some JavaScript to run after the next *natural*
130 style and layout flush, try:
133 // Suppose we want to get the computed "display" style of some node without
134 // causing a style flush. We could do it this way:
135 async function nodeIsDisplayNone(node) {
136 let display = await window.promiseDocumentFlushed(() => {
137 // Do _not_ under any circumstances write to the DOM in one of these
139 return window.getComputedStyle(node).display;
142 return display == "none";
145 See [Detecting and avoiding synchronous reflow](#detecting-and-avoiding-synchronous-reflow)
146 for a more advanced example of getting layout information, and then
147 setting it safely, without causing flushes.
149 bestpractices.html#detecting-and-avoiding-synchronous-reflow
152 *promiseDocumentFlushed* is only available to privileged script, and
153 should be called on the inner window of a top-level frame. Calling it on
154 the outer window of a subframe is not supported, and calling it from
155 within the inner window of a subframe might cause the callback to fire
156 even though a style and layout flush will still be required. These
157 gotchas should be fixed by
158 [bug 1441173](https://bugzilla.mozilla.org/show_bug.cgi?id=1441173).
160 For now, it is up to you as the consumer of this API to not accidentally
161 write to the DOM within the *promiseDocumentFlushed* callback. Doing
162 so might cause flushes to occur for other *promiseDocumentFlushed*
163 callbacks that are scheduled to fire in the same tick of the refresh
165 [bug 1441168](https://bugzilla.mozilla.org/show_bug.cgi?id=1441168)
166 tracks work to make it impossible to modify the DOM within a
167 *promiseDocumentFlushed* callback.
169 ### Writing tests to ensure you don’t add more synchronous style flushes
171 Unlike reflow, there isn’t a “observer” mechanism for style
172 recalculations. However, as of Firefox 49, the
173 *nsIDOMWindowUtils.elementsRestyled* attribute records a count of how
174 many style calculations have occurred for a particular DOM window.
176 It should be possible to write a test that gets the
177 *nsIDOMWindowUtils* for a browser window, records the number of
178 styleFlushes, then **synchronously calls the function** that you want to
179 test, and immediately after checks the styleFlushes attribute again. If
180 the value went up, your code caused synchronous style flushes to occur.
182 Note that your test and function *must be called synchronously* in order
183 for this test to be accurate. If you ever go back to the event loop (by
184 yielding, waiting for an event, etc), style flushes unrelated to your
185 code are likely to run, and your test will give you a false positive.
187 ## Detecting and avoiding synchronous reflow
189 This is also sometimes called “sync layout”, "sync layout flushes" or
190 “sync layout calculations”
192 *Sync reflow* is a term bandied about a lot, and has negative
193 connotations. It's not unusual for an engineer to have only the vaguest
194 sense of what it is—and to only know to avoid it. This section will
195 attempt to demystify things.
197 The first time a document (XUL or HTML) loads, we parse the markup, and
198 then apply styles. Once the styles have been calculated, we then need to
199 calculate where things are going to be placed on the page. This layout
200 step can be seen in the “16ms” pipeline graphic above, and occurs just
201 before we paint things to be composited for the user to see.
203 It is expected that over time, script will update the DOM, requiring us
204 to recalculate styles, and then update layout. Normally, however, the
205 changes to the DOM just result in the standard style calculation that
206 occurs immediately after the JavaScript has finished running during the
209 ### Interruptible reflow
211 Since [the early days](https://bugzilla.mozilla.org/show_bug.cgi?id=67752), Gecko has
212 had the notion of interruptible reflow. This is a special type of
213 **content-only** reflow that checks at particular points whether or not
214 it should be interrupted (usually to respond to user events).
216 Because **interruptible reflows can only be interrupted when laying out
217 content, and not chrome UI**, the rest of this section is offered only
220 When an interruptible reflow is interrupted, what really happens is that
221 certain layout operations can be skipped in order to paint and process
224 When an interruptible reflow is interrupted, the best-case scenario is
225 that all layout is skipped, and the layout operation ends.
227 The worst-case scenario is that none of the layout can be skipped
228 despite being interrupted, and the entire layout calculation occurs.
230 Reflows that are triggered "naturally" by the 16ms tick are all
231 considered interruptible. Despite not actually being interuptible when
232 laying out chrome UI, striving for interruptible layout is always good
233 practice because uninterruptible layout has the potential to be much
234 worse (see next section).
236 **To repeat, only interruptible reflows in web content can be
239 ### Uninterruptible reflow
241 Uninterruptible reflow is what we want to **avoid at all costs**.
242 Uninterruptible reflow occurs when some DOM node’s styles have changed
243 such that the size or position of one or more nodes in the document will
244 need to be updated, and then **JavaScript asks for the size or position
245 of anything**. Since everything is pending a reflow, the answer isn't
246 available, so everything stalls until the reflow is complete and the
247 script can be given an answer. Flushing layout also means that styles
248 must be flushed to calculate the most up-to-date state of things, so
249 it's a double-whammy.
251 Here’s a simple example, cribbed from [this blog post by Paul
252 Rouget](http://paulrouget.com/e/fxoshud):
255 div1.style.margin = "200px"; // Line 1
256 var height1 = div1.clientHeight; // Line 2
257 div2.classList.add("foobar"); // Line 3
258 var height2 = div2.clientHeight; // Line 4
259 doSomething(height1, height2); // Line 5
261 At line 1, we’re setting some style information on a DOM node that’s
262 going to result in a reflow - but (at just line 1) it’s okay, because
263 that reflow will happen after the style calculation.
265 Note line 2 though - we’re asking for the height of some DOM node. This
266 means that Gecko needs to synchronously calculate layout (and styles)
267 using an uninterruptible reflow in order to answer the question that
268 JavaScript is asking (“What is the *clientHeight* of *div1*?”).
270 It’s possible for our example to avoid this synchronous, uninterruptible
271 reflow by moving lines 2 and 4 above line 1. Assuming there weren’t any
272 style changes requiring size or position recalculation above line 1, the
273 *clientHeight* information should be cached since the last reflow, and
274 will not result in a new layout calculation.
276 If you can avoid querying for the size or position of things in
277 JavaScript, that’s the safest option—especially because it’s always
278 possible that something earlier in this tick of JavaScript execution
279 caused a style change in the DOM without you knowing it.
281 Note that given the same changes to the DOM of a chrome UI document, a
282 single synchronous uninterruptible reflow is no more computationally
283 expensive than an interruptible reflow triggered by the 16ms tick. It
284 is, however, advantageous to strive for reflow to only occur in the one
285 place (the layout step of the 16ms tick) as opposed to multiple times
286 during the 16ms tick (which has a higher probability of running through
289 ### How do I avoid triggering uninterruptible reflow?
291 Here's a [list of things that JavaScript can ask for that can cause
292 uninterruptible reflow](https://gist.github.com/paulirish/5d52fb081b3570c81e3a), to
293 help you think about the problem. Note that some items in the list may
294 be browser-specific or subject to change, and that an item not occurring
295 explicitly in the list doesn't mean it doesn't cause reflow. For
296 instance, at time of writing accessing *event.rangeOffset* [triggers
297 reflow](https://searchfox.org/mozilla-central/rev/6bfadf95b4a6aaa8bb3b2a166d6c3545983e179a/dom/events/UIEvent.cpp#215-226)
298 in Gecko, and does not occur in the earlier link. If you're unsure
299 whether something causes reflow, check!
301 Note how abundant the properties in that first list are. This means that
302 when enumerating properties on DOM objects (e.g. elements/nodes, events,
303 windows, etc.) **accessing the value of each enumerated property will
304 almost certainly (accidentally) cause uninterruptible reflow**, because
305 a lot of DOM objects have one or even several properties that do so.
307 If you require size or position information, you have a few options.
309 [bug 1434376](https://bugzilla.mozilla.org/show_bug.cgi?id=1434376)
310 has landed a helper in the window binding to make it easier for
311 privileged code to queue up JavaScript to run when we know that the DOM
312 is not dirty, and size, position, and style information is cheap to
317 async function matchWidth(elem, otherElem) {
318 let width = await window.promiseDocumentFlushed(() => {
319 // Do _not_ under any circumstances write to the DOM in one of these
321 return elem.clientWidth;
324 requestAnimationFrame(() => {
325 otherElem.style.width = `${width}px`;
329 Please see the section on *promiseDocumentFlushed* in [Detecting and
330 avoiding synchronous style flushes](#detecting-and-avoiding-synchronous-style-flushes)
331 for more information on how to use the API.
333 Note that queries for size and position information are only expensive
334 if the DOM has been written to. Otherwise, we're doing a cheap look-up
335 of cached information. If we work hard to move all DOM writes into
336 *requestAnimationFrame()*, then we can be sure that all size and
337 position queries are cheap.
339 It's also possible (though less infallible than
340 *promiseDocumentFlushed*) to queue JavaScript to run very soon after
341 the frame has been painted, where the likelihood is highest that the DOM
342 has not been written to, and layout and style information queries are
343 still cheap. This can be done by using a *setTimeout* or dispatching a
344 runnable inside a *requestAnimationFrame* callback, for example:
347 requestAnimationFrame(() => {
349 // This code will be run ASAP after Style and Layout information have
350 // been calculated and the paint has occurred. Unless something else
351 // has dirtied the DOM very early, querying for style and layout information
352 // here should be cheap.
356 // Or, if you are running in privileged JavaScript and want to avoid the timer overhead,
357 // you could also use:
359 requestAnimationFrame(() => {
360 Services.tm.dispatchToMainThread(() => {
361 // Same-ish as above.
365 This also implies that *querying for size and position information* in
366 *requestAnimationFrame()* has a high probability of causing a
369 ### Other useful methods
371 Below you'll find some suggestions for other methods which may come in
372 handy when you need to do things without incurring synchronous reflow.
373 These methods generally return the most-recently-calculated value for
374 the requested value, which means the value may no longer be current, but
375 may still be "close enough" for your needs. Unless you need precisely
376 accurate information, they can be valuable tools in your performance
379 #### nsIDOMWindowUtils.getBoundsWithoutFlushing()
381 *getBoundsWithoutFlushing()* does exactly what its name suggests: it
382 allows you to get the bounds rectangle for a DOM node contained in a
383 window without flushing layout. This means that the information you get
384 is potentially out-of-date, but allows you to avoid a sync reflow. If
385 you can make do with information that may not be quite current, this can
388 #### nsIDOMWindowUtils.getRootBounds()
390 Like *getBoundsWithoutFlushing()*, *getRootBounds()* lets you get
391 the dimensions of the window without risking a synchronous reflow.
393 #### nsIDOMWindowUtils.getScrollXY()
395 Returns the window's scroll offsets without taking the chance of causing
398 ### Writing tests to ensure you don’t add more unintentional reflow
401 [nsIReflowObserver](https://dxr.mozilla.org/mozilla-central/source/docshell/base/nsIReflowObserver.idl)
402 lets us detect both interruptible and uninterruptible reflows. A number
403 of tests have been written that exercise various functions of the
404 browser [opening tabs](http://searchfox.org/mozilla-central/rev/78cefe75fb43195e7f5aee1d8042b8d8fc79fc70/browser/base/content/test/general/browser_tabopen_reflows.js),
405 [opening windows](http://searchfox.org/mozilla-central/source/browser/base/content/test/general/browser_windowopen_reflows.js)
406 and ensure that we don’t add new uninterruptible reflows accidentally
407 while those actions occur.
409 You should add tests like this for your feature if you happen to be
412 ## Detecting over-painting
414 Painting is, in general, cheaper than both style calculation and layout
415 calculation; still, the more you can avoid, the better. Generally
416 speaking, the larger an area that needs to be repainted, the longer it
417 takes. Similarly, the more things that need to be repainted, the longer
420 If a profile says a lot of time is spent in painting or display-list building,
421 and you're unsure why, consider talking to our always helpful graphics team in
422 the [gfx room](https://chat.mozilla.org/#/room/%23gfx:mozilla.org) on
423 [Matrix](https://wiki.mozilla.org/Matrix), and they can probably advise you.
425 Note that a significant number of the graphics team members are in the US
426 Eastern Time zone (UTC-5 or UTC-4 during Daylight Saving Time), so let that
427 information guide your timing when you ask questions in the
428 [gfx room](https://chat.mozilla.org/#/room/%23gfx:mozilla.org).
430 ## Adding nodes using DocumentFragments
432 Sometimes you need to add several DOM nodes as part of an existing DOM
433 tree. For example, when using XUL *\<xul:menupopup\>s*, you often have
434 script which dynamically inserts *\<xul:menuitem\>s*. Inserting items
435 into the DOM has a cost. If you're adding a number of children to a DOM
436 node in a loop, it's often more efficient to batch them into a single
437 insertion by creating a *DocumentFragment*, adding the new nodes to
438 that, then inserting the *DocumentFragment* as a child of the desired
441 A *DocumentFragment* is maintained in memory outside the DOM itself,
442 so changes don't cause reflow. The API is straightforward:
444 1. Create the *DocumentFragment* by calling
445 *Document.createDocumentFragment()*.
447 2. Create each child element (by calling *Document.createElement()*
448 for example), and add each one to the fragment by calling
449 *DocumentFragment.appendChild()*.
451 3. Once the fragment is populated, append the fragment to the DOM by
452 calling *appendChild()* on the parent element for the new elements.
454 This example has been cribbed from [davidwalsh’s blog
455 post](https://davidwalsh.name/documentfragment):
458 // Create the fragment
460 var frag = document.createDocumentFragment();
462 // Create numerous list items, add to fragment
464 for(var x = 0; x < 10; x++) {
465 var li = document.createElement("li");
466 li.innerHTML = "List item " + x;
467 frag.appendChild(li);
470 // Mass-add the fragment nodes to the list
472 listNode.appendChild(frag);
474 The above is strictly cheaper than individually adding each node to the
477 ## The Gecko profiler add-on is your friend
479 The Gecko profiler is your best friend when diagnosing performance
480 problems and looking for bottlenecks. There’s plenty of excellent
481 documentation on MDN about the Gecko profiler:
483 - [Basic instructions for gathering and sharing a performance profile](reporting_a_performance_problem.md)
485 - [Advanced profile analysis](https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Profiling_with_the_Built-in_Profiler)
487 ## Don’t guess—measure.
489 If you’re working on a performance improvement, this should go without
490 saying: ensure that what you care about is actually improving by
491 measuring before and after.
493 Landing a speculative performance enhancement is the same thing as
494 landing speculative bug fixes—these things need to be tested. Even if
495 that means instrumenting a function with a *Date.now()* recording at
496 the entrance, and another *Date.now()* at the exit points in order to
497 measure processing time changes.
499 Prove to yourself that you’ve actually improved something by measuring
502 ### Use the performance API
505 API](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API)
506 is very useful for taking high-resolution measurements. This is usually
507 much better than using your own hand-rolled timers to measure how long
508 things take. You access the API through *Window.performance*.
510 Also, the Gecko profiler back-end is in the process of being modified to
511 expose things like markers (from *window.performance.mark()*).
513 ### Use the compositor for animations
515 Performing animations on the main thread should be treated as
516 **deprecated**. Avoid doing it. Instead, animate using
517 *Element.animate()*. See the article [Animating like you just don't
518 care](https://hacks.mozilla.org/2016/08/animating-like-you-just-dont-care-with-element-animate/)
519 for more information on how to do this.
521 ### Explicitly define start and end animation values
523 Some optimizations in the animation code of Gecko are based on an
524 expectation that the *from* (0%) and the *to* (100%) values will be
525 explicitly defined in the *@keyframes* definition. Even though these
526 values may be inferred through the use of initial values or the cascade,
527 the offscreen animation optimizations are dependent on the explicit
528 definition. See [this comment](https://bugzilla.mozilla.org/show_bug.cgi?id=1419096#c18)
529 and a few previous comments on that bug for more information.
531 ## Use IndexedDB for storage
533 [AppCache](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/en-US/docs/Web/HTML/Using_the_application_cache)
535 [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage)
536 are synchronous storage APIs that will block the main thread when you
537 use them. Avoid them at all costs!
539 [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB)
540 is preferable, as the API is asynchronous (all disk operations occur off
541 of the main thread), and can be accessed from web workers.
543 IndexedDB is also arguably better than storing and retrieving JSON from
544 a file—particularly if the JSON encoding or decoding is occurring on the
545 main thread. IndexedDB will do JavaScript object serialization and
546 deserialization for you using the [structured clone
547 algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)
548 meaning that you can stash [things like maps, sets, dates, blobs, and
549 more](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#Supported_types)
550 without having to do conversions for JSON compatibility.
552 A Promise-based wrapper for IndexedDB,
553 [IndexedDB.sys.mjs](http://searchfox.org/mozilla-central/source/toolkit/modules/IndexedDB.sys.mjs)
554 is available for chrome code.
556 ## Test on weak hardware
558 For the folks paid to work on Firefox, we tend to have pretty powerful
559 hardware for development. This is great, because it reduces build times,
560 and means we can do our work faster.
562 We should remind ourselves that the majority of our user base is
563 unlikely to have similar hardware. Look at the [Firefox Hardware
564 Report](https://data.firefox.com/dashboard/hardware) to get
565 a sense of what our users are working with. Test on slower machines to
566 make it more obvious to yourself if what you’ve written impacts the
567 performance of the browser.
569 ## Consider loading scripts with the subscript loader asynchronously
571 If you've ever used the subscript loader, you might not know that it can
572 load scripts asynchronously, and return a Promise once they're loaded.
576 Services.scriptloader.loadSubScriptWithOptions(myScriptURL, { async: true }).then(() => {
577 console.log("Script at " + myScriptURL + " loaded asynchronously!");