Firefox

Firefox フロントエンドエンジニアのためのパフォーマンスベストプラクティス

この翻訳は不完全です。英語から この記事を翻訳 してください。

可能であればメインスレッドを避けること

メインスレッドはユーザーイベントを処理し、描画を行います。また、多くの JavaScript はメインスレッドで動きます。

メインスレッドを避けるほどに、ユーザーイベントや描画、応答が素早くできます。

メインスレッドから何か計算する必要がある場合、 Worker を使うことをおすすめします。また、より高い権限が必要な場合は ChromeWorker (Firefoxのみ)を使うことを検討してください。

requestIdleCallback

どうしてもメインスレッドで何らかの長い処理をしないといけない場合、おそらく存在するであろうユーザーが何もしない空き時間に、小さなパーツに分割して実行することを検討してください。

RequestIdleCallback ではこちらが役に立つでしょう。 こちらの Hacks ブログをチェックしてください。

そして、いつか、DOM コンテキスト以外でも可能となるでしょう!

Hide your panels

If you’re adding a new <popup> or <panel> to a XUL document, set the hidden=”true” attribute on it by default. That way, the binding is applied on demand, and we can save time when constructing the XUL document.

Get familiar with the pipeline that gets pixels to the screen

This is the pipeline that a browser uses to get pixels to the screen.

The above image is used under Creative Commons Attribution 3.0, courtesy of this page from our friends at Google, which itself is well worth the read.

For 60fps, the above needs to occur in 16ms or less.

Note that the requestAnimationFrame API allows you to queue up JavaScript to run very soon after a trip through this pipeline has been completed. This is useful because the last trip through the pipeline likely cached layout and style information which (assuming the DOM hasn't been dirtied somehow) should still be valid and cheap to access (see the sections below on synchronous style and layout flushes).

Detecting and Avoiding Synchronous Style Flushes

What are Style Flushes?

When CSS is applied to a document (HTML or XUL, it doesn’t matter), we do a calculation to determine what CSS styles will apply to each element.

This will happen during the first time the page loads and CSS is first applied, but can happen again if JavaScript modifies the DOM - for example, by changing DOM node attributes (either directly or via APIs like classList.add / classList.remove / classList.toggle), or adding / removing / moving DOM nodes. Note that because styles are normally scoped to the entire document, the cost of doing these style calculations is proportional to the number of DOM nodes in the document (and the number of styles being applied).

It is expected that over time, script will update the DOM, requiring us to recalculate styles. Normally, however, the changes to the DOM just result in the standard style calculation that occurs immediately after the JavaScript has finished running during the 16ms window.

It is possible for JavaScript to force multiple, synchronous style calculations (or “style flushes”) to occur during the 16ms window, which greatly increases the probability of going over the 16ms limit, causing us to skip painting one or more frames. Skipping frames is called jank.

Generally speaking, you force a synchronous style flush any time you query for style information after the DOM has changed. Depending on whether or not the style information you’re asking for has something to do with size or position, you may also cause a layout re-calculation (also referred to as “layout flush” or “reflow”), which is also an expensive step (see the section on Detecting and Avoiding Synchronous Reflow below).

To avoid this: avoid reading style information if you can. If you must read style information, do so at the very beginning of the frame before any changes to the DOM have occurred since the last style flush (perhaps by using requestAnimationFrame to set a callback at the start of a frame). At the start of a frame, style values are cached from the last calculation and more cheaply accessed when the document hasn’t yet undergone any change.

Writing tests to ensure you don’t add more synchronous style flushes

Unlike reflow, there isn’t a “observer” mechanism for style recalculations. There is, however, an attribute on nsIDOMWindowUtils that records a count of how many style calculations have occurred for a particular DOM window.

It should be possible to write a test that gets the nsIDOMWindowUtils for a browser window, records the count of styleFlushes, then synchronously calls the function that you want to test, and immediately after checks the styleFlushes attribute again. If the value went up, your code caused synchronous style flushes to occur.

Note that your test and function must be called synchronously in order for this test to be accurate. If you ever go back to the event loop (by yielding, waiting for an event, etc), style flushes unrelated to your code are likely to run, and your test will give you a false positive.

Detecting and Avoiding Synchronous Reflow

This is also sometimes called “sync Layout” or “sync Layout calculations”

“Sync Reflow” is a term bandied about a lot, and has negative connotations. It's not unusual for an engineer to have only the vaguest sense of what it is - and to only know to avoid it.

This section will attempt to demystify things.

The first time a document (XUL or HTML) loads, we parse the markup, and then apply styles. Once the styles have been calculated, we then need to calculate where things are going to be placed on the page. This layout step can be seen in the “16ms” pipeline graphic above, and occurs just before we paint things to be composited for the user to see.

It is expected that over time, script will update the DOM, requiring us to recalculate styles, and then update layout. Normally, however, the changes to the DOM just result in the standard style calculation that occurs immediately after the JavaScript has finished running during the 16ms window.

Also note that since the early days, Gecko has had the notion of interruptible reflow. This means that size and position calculations can be broken up over several of those 16ms windows. Gecko will decide when an interruptible reflow has taken too long, and then bail out to paint. It will continue the reflow in the next 16ms window. Interruptible reflow is fine. Interruptible reflow only happens during initial document load.

Uninterruptible reflow is what we want to avoid at all costs. Uninterruptible reflow occurs when some DOM node’s styles have changed such that the size or position of one or more nodes in the document will need to be updated, and then JavaScript asks for the size or position of anything. Here’s a comprehensive list of things that JavaScript can ask for that can cause uninterruptible reflow.

Here’s a simple example, cribbed from this blog post by Paul Rouget:

div1.style.margin = "200px";        // Line 1
var height1 = div1.clientHeight;    // Line 2
div2.classList.add("foobar");       // Line 3
var height2 = div2.clientHeight;    // Line 4
doSomething(height1, height2);      // Line 5

At line 1, we’re setting some style information on a DOM node that’s going to result in a reflow - but (at just line 1) it’s okay, because that reflow will happen after the style calculation.

Note line 2 though - we’re asking for the height of some DOM node. This means that Gecko needs to synchronously calculate layout using an uninterruptible reflow in order to answer the question that JavaScript is asking (“What is the clientHeight of div1?”).

It’s possible for our example to avoid this synchronous, uninterruptible reflow by moving lines 2 and 4 above line 1. Assuming there weren’t any style changes requiring size or position recalculation above line 1, the clientHeight information should be cached since the last reflow, and will not result in a new layout calculation.

If you can avoid querying for the size or position of things in JavaScript, that’s the safest option - especially because it’s always possible that something earlier in this tick of JS execution caused a style change in the DOM without you knowing it.

You could also potentially move your query into a requestAnimationFrame callback. This will run your code after the last frame is painted, and all of the layout data should be cached. Assuming no DOM properties affecting size or position have been changed in the meantime, accessing those cached values should be free.

nsIDOMWindowUtils.getBoundsWithoutFlushing

getBoundsWithoutFlushing does exactly what it says - it allows you to get the rect for some DOM node in a window without flushing layout. This means that the information you get is potentially stale, but allows you to avoid a sync reflow.

nsIDOMWindowUtils.getRootBounds

Similar to above - but allows you to get dimensions of the containing window without causing a sync reflow.

nsIDOMWindowUtils.getScrollXY

Similar to above - but allows you to get the window scroll offsets without causing a sync reflow.

Writing tests to ensure you don’t add more unintentional reflow

We have something called nsIReflowObserver, which allows us to detect both interruptible and uninterruptible reflows. A number of tests have been written that exercise various functions of the browser (opening tabs, opening windows) and ensures that we don’t add new uninterruptible reflows accidentally while those actions occur.

You should add tests like this for your feature if you happen to be touching the DOM.

Detecting Over-painting with Paint Flashing

Painting is, in general, cheaper than both style calculation and layout calculation - but the more you can avoid, the better.

Generally speaking, the larger an area that needs to be repainted, the longer it takes. Similarly, the more things that need to be repainted, the longer it takes.

Our graphics team has added a handy feature to help you detect when and where paints are occurring. This feature is called “paint flashing”, and it can be activated for both web content and the browser chrome. Paint flashing tints each region being painted with a randomly selected colour so that it’s more easy to see what on the screen is being painted.

You can activate paint flashing for browser chrome by setting nglayout.debug.paint_flashing_chrome to true.

You can activate paint flashing for web content by setting nglayout.debug.paint_flashing to true.

Now exercise your function and see what’s painting. See a lot of flashing / colours? That means a lot of painting is going on.

Painting occurs on the main thread. The more things we can take off of the main thread, the better. If you’re overpainting, it’s a good idea to figure out why.

Perhaps you’re animating something that requires a repaint? For example, transitioning the background-color of a DOM node from red to blue will result in a repaint for every frame of the animation, and paint flashing will reveal that. Consider using a different animation that can be accelerated by the GPU - these occur off of the main thread, and have a much higher probability of running at 60fps (see the section below labeled “Rely on the Compositor for Animations” for further details).

Perhaps you’re touching some DOM nodes in such a way that unexpected repaints are occurring in an area that don’t need it. Best to investigate and try to remove those as best you can. Sometimes, our graphics layer invalidates regions in ways that might not be clear to you, and a section outside of the thing that just repainted will also repaint. Sometimes this can be addressed by ensuring that the thing changing is on its own layer (though this comes at a memory cost). You can put something on its own layer by setting its z-index, or by setting the will-change style on the node, though this should be used sparingly.

If you’re unsure why something is repainting, consider talking to our always helpful Graphics team in the #gfx IRC channel, and they can probably advise you. Note that a significant number of the Graphics team members are in the Eastern Time zone, so let that information guide your timing when you ask questions in #gfx.

Adding Nodes with documentFragment

There is periodically the need to add a series of DOM nodes as children to another DOM node. For example, for things like our XUL menupopup’s, one often has JavaScript dynamically inserting menuitem’s.

Inserting items into the DOM comes with a cost. If you’re adding a number of children to a DOM node in a loop, it’s often cheaper to batch those adds into a single insertion.

Thanks to the createDocumentFragment API, this is very straight-forward.

This example has been cribbed from davidwalsh’s blog post:

// Create the fragment
var frag = document.createDocumentFragment();
// Create numerous list items, add to fragment
for(var x = 0; x < 10; x++) {
    var li = document.createElement("li");
    li.innerHTML = "List item " + x;
    frag.appendChild(li);
}

// Mass-add the fragment nodes to the list
listNode.appendChild(frag);

The above is strictly cheaper that individually adding each node to the DOM.

The Gecko Profiler Add-on is your friend

The Gecko Profiler is your best friend when diagnosing performance problems and looking for bottlenecks.

There’s plenty of excellent documentation on MDN about the Gecko Profiler:

Don’t Guess. Measure.

If you’re working on a performance improvement, this should go without saying: ensure that what you care about is actually improving by measuring before and after.

Landing a speculative performance enhancement is the same thing as landing speculative bug fixes - these things need to be tested. Even if that means instrumenting a function with a Date.now() recording at the entrance, and a Date.now() at the exits in order to measure processing time changes.

Prove to yourself that you’ve actually improved something by measuring before and after.

window.performance

The Performance API is very useful for taking high-resolution measurements. This is usually much better than using your own hand-rolled timers to measure how long things take.

Also, the Gecko Profiler back-end is in the process of being modified to expose things like markers (from window.performance.mark).

Rely on the Compositor for animations. Main thread animation should be treated as deprecated.

Here’s an excellent article about doing that.

Use IndexedDB for Storage

AppCache and LocalStorage are synchronous storage APIs that will block the main thread when you use them. Avoid them at all costs!

IndexedDB is preferable, as the API is asynchronous (all disk operations occur off of the main thread), and can be accessed from Web Workers.

IndexedDB is also arguably better than storing and retrieving JSON from a file - particularly if the JSON encoding or decoding is occurring on the main thread. IndexedDB will do JS object serialization and deserialization for you using the Structured Clone algorithm, meaning that you can stash things like Maps, Sets, Dates, Blobs, and more, without having to do conversions for JSON compatibility.

A Promise-based wrapper for IndexedDB, IndexedDB.jsm, is available for chrome code.

Test on weak hardware

For the folks paid to work on Firefox, we tend to have pretty powerful hardware for development. This is great, because it reduces build times, and means we can do our work faster.

We should remind ourselves that the majority of our user base is unlikely to have similar hardware. Look at the Firefox Hardware Report to get a sense of what our users are working with. Test on slower machines to make it more obvious to yourself if what you’ve written impacts the performance of the browser.

 

ドキュメントのタグと貢献者

 最終更新者: WhiteHawk-taka,