[Article Sections]
Preventing Forced Reflows with Strategic JavaScript: II
This is part 2 of a two-part article on forced reflows.
Code
Better than pseudo-code is actual code that creates forced reflows and provides coding solutions for direct examination. Below is the code we’ll be using for the remainder of this article. You may run it directly from this site at code-examples/forced-reflow-solutions.html. However, it would be best to save it as forced-reflow-solutions.html and run it on a localhost machine to test modifications to the code outside what’s discussed here. Regardless, in a modern Chrome browser (version 61 as of this writing) in incognito mode, and with the DevTools open, run this code:
<!DOCTYPE html> <html> <head> <meta charset=utf-8> <title>Forced Reflow Solutions</title> <style> body { font: 16px 'Arial', sans-serif; overflow-y: scroll; } h1 { text-align: center; font: 20px 'Arial', sans-serif; } h2 { text-align: center; font: 16px 'Arial', sans-serif; } .buttons, .list div { display: table; margin: 0 auto; text-align: center; } .buttons { margin: 1em auto; } .ex-text { display: none; } .explanation div { margin: 1em auto; text-align: center; } .explanation .display-block { display: block; } .list div { background-color: lightBlue; } </style> </head> <body> <h1>Run This in Chrome Incognito Mode with DevTools Open</h1> <h2>Generate DIVs with random text, double width of each DIV relative to text width</h2> <div class=buttons> <button value=1>Method 1</button> <button value=2>Method 2</button> <button value=3>Method 3</button> <button value=4>Method 4</button> <button value=5>Method 5</button> </div> <div class=explanation> <div class=ex-text id=1>Method 1: Resize DIVs in same loop where they're created.</div> <div class=ex-text id=2>Method 2: Resize DIVs in separate loop, but get clientWidth of DIV and set width of DIV in same statement.</div> <div class=ex-text id=3>Method 3: Resize DIVs in separate loops: one to collect clientWidths, the other to set widths relative to them.</div> <div class=ex-text id=4>Method 4: Same as method 3, but wrapping each loop in a requestAnimationFrame.</div> <div class=ex-text id=5>Method 5: Same as method 4, except using FastDom to separate measurements from mutations.</div> </div> <div class=list></div> <!-- See: https://github.com/wilsonpage/fastdom --> <script src="https://cdnjs.cloudflare.com/ajax/libs/fastdom/1.0.6/fastdom.min.js"></script> <script> (function () { 'use strict'; function makeList(method) { ///////////////////// // Declare variables: ///////////////////// var div; var divArr; var divWidthArr; var fragment; var listDiv; var randomText; //////////////////////// // Initialize variables: //////////////////////// divArr = []; divWidthArr = []; listDiv = document.querySelector('.list'); ///////////// // Prep Work: ///////////// // Clear out previous console display: console.clear(); // Hide previous explanation: Array.prototype.forEach.call(document.querySelectorAll('.ex-text'), function (element) { element.classList.remove('display-block'); }); // Display current explanation: document.getElementById(method).classList.add('display-block'); // Remove any previous contents of .list DIV: while (listDiv.firstChild) { listDiv.removeChild(listDiv.firstChild); } //////////////// // Main Routine: //////////////// // Create documentFragment for methods 2-5: if (method !== 1) { fragment = document.createDocumentFragment(); } // Create DIVs with random text and insert in list DIV: for (var i = 0; i < 1000; i++) { // Create DIV: div = document.createElement('DIV'); // Create random text: randomText = Math.random().toString(36).substr(2); // Put random text in DIV: div.innerHTML = randomText; // Save reference to DIV in divArr divArr.push(div); // Next, double the size of each DIV: /////////////////////////////////////////// // METHOD 1: forced reflows. // Measure (clientWidth) and mutate (width) // in same statement; also performed once // per loop iteration: /////////////////////////////////////////// if (method === 1) { listDiv.appendChild(div); div.style.width = (div.clientWidth * 2) + 'px'; } else { fragment.appendChild(div); } } // For all methods except #1, transfer the fragment contents to the DOM: if (method !== 1) { listDiv.appendChild(fragment); } ///////////////////////////////////////// // METHOD 2: forced reflows. // SLOW. // Measure and mutate in dedicated loop, // but still performed in same statement: ///////////////////////////////////////// if (method === 2) { divArr.forEach(function (div) { div.style.width = (div.clientWidth * 2) + 'px'; }); } /////////////////////////////////////// // METHOD 3: occasional forced reflows. // FASTER. // Measure and mutate operations placed // in separate loops: /////////////////////////////////////// if (method === 3) { // MEASURE: Fill the divWidthArr[] with each DIV's clientWidth: divArr.forEach(function (div) { divWidthArr.push(div.clientWidth); }); // MUTATE: Set each DIV's width to twice its clientWidth: divArr.forEach(function (div, index) { div.style.width = (divWidthArr[index] * 2) + 'px'; }); } //////////////////////////////////////// // METHOD 4: no forced reflows. // Measure and mutate operations placed // in separate loops, both in // callback to requestAnimationFrame(): //////////////////////////////////////// if (method === 4) { window.requestAnimationFrame(function () { // MEASURE: Fill the divWidthArr[] with each DIV's clientWidth: divArr.forEach(function (div) { divWidthArr.push(div.clientWidth); }); // MUTATE: Set each DIV's width to twice its clientWidth: divArr.forEach(function (div, index) { div.style.width = (divWidthArr[index] * 2) + 'px'; }); }); } /////////////////////////////// // METHOD 5: no forced reflows. // Distinguish measure operations from mutation operations, // nesting fastdom.mutate within fastdom.measure /////////////////////////////// if (method === 5) { // MEASURE: window.fastdom.measure(function () { divArr.forEach(function (div) { divWidthArr.push(div.clientWidth); }); // MUTATE: window.fastdom.mutate(function () { divArr.forEach(function (div, index) { div.style.width = (divWidthArr[index] * 2) + 'px'; }); }); }); } } // Add click listeners to buttons; call makeList() with button value as argument: Array.prototype.forEach.call(document.querySelectorAll('button'), function (button) { button.addEventListener('click', function () { // Wrap makeList call in rAF: window.requestAnimationFrame(function () { makeList(parseInt(button.value)); }); }); }); }()); </script> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset=utf-8> <title>Forced Reflow Solutions</title> <style> body { font: 16px 'Arial', sans-serif; overflow-y: scroll; } h1 { text-align: center; font: 20px 'Arial', sans-serif; } h2 { text-align: center; font: 16px 'Arial', sans-serif; } .buttons, .list div { display: table; margin: 0 auto; text-align: center; } .buttons { margin: 1em auto; } .ex-text { display: none; } .explanation div { margin: 1em auto; text-align: center; } .explanation .display-block { display: block; } .list div { background-color: lightBlue; } </style> </head> <body> <h1>Run This in Chrome Incognito Mode with DevTools Open</h1> <h2>Generate DIVs with random text, double width of each DIV relative to text width</h2> <div class=buttons> <button value=1>Method 1</button> <button value=2>Method 2</button> <button value=3>Method 3</button> <button value=4>Method 4</button> <button value=5>Method 5</button> </div> <div class=explanation> <div class=ex-text id=1>Method 1: Resize DIVs in same loop where they're created.</div> <div class=ex-text id=2>Method 2: Resize DIVs in separate loop, but get clientWidth of DIV and set width of DIV in same statement.</div> <div class=ex-text id=3>Method 3: Resize DIVs in separate loops: one to collect clientWidths, the other to set widths relative to them.</div> <div class=ex-text id=4>Method 4: Same as method 3, but wrapping each loop in a requestAnimationFrame.</div> <div class=ex-text id=5>Method 5: Same as method 4, except using FastDom to separate measurements from mutations.</div> </div> <div class=list></div> <!-- See: https://github.com/wilsonpage/fastdom --> <script src="https://cdnjs.cloudflare.com/ajax/libs/fastdom/1.0.6/fastdom.min.js"></script> <script> (function () { 'use strict'; function makeList(method) { ///////////////////// // Declare variables: ///////////////////// var div; var divArr; var divWidthArr; var fragment; var listDiv; var randomText; //////////////////////// // Initialize variables: //////////////////////// divArr = []; divWidthArr = []; listDiv = document.querySelector('.list'); ///////////// // Prep Work: ///////////// // Clear out previous console display: console.clear(); // Hide previous explanation: Array.prototype.forEach.call(document.querySelectorAll('.ex-text'), function (element) { element.classList.remove('display-block'); }); // Display current explanation: document.getElementById(method).classList.add('display-block'); // Remove any previous contents of .list DIV: while (listDiv.firstChild) { listDiv.removeChild(listDiv.firstChild); } //////////////// // Main Routine: //////////////// // Create documentFragment for methods 2-5: if (method !== 1) { fragment = document.createDocumentFragment(); } // Create DIVs with random text and insert in list DIV: for (var i = 0; i < 1000; i++) { // Create DIV: div = document.createElement('DIV'); // Create random text: randomText = Math.random().toString(36).substr(2); // Put random text in DIV: div.innerHTML = randomText; // Save reference to DIV in divArr divArr.push(div); // Next, double the size of each DIV: /////////////////////////////////////////// // METHOD 1: forced reflows. // Measure (clientWidth) and mutate (width) // in same statement; also performed once // per loop iteration: /////////////////////////////////////////// if (method === 1) { listDiv.appendChild(div); div.style.width = (div.clientWidth * 2) + 'px'; } else { fragment.appendChild(div); } } // For all methods except #1, transfer the fragment contents to the DOM: if (method !== 1) { listDiv.appendChild(fragment); } ///////////////////////////////////////// // METHOD 2: forced reflows. // SLOW. // Measure and mutate in dedicated loop, // but still performed in same statement: ///////////////////////////////////////// if (method === 2) { divArr.forEach(function (div) { div.style.width = (div.clientWidth * 2) + 'px'; }); } /////////////////////////////////////// // METHOD 3: occasional forced reflows. // FASTER. // Measure and mutate operations placed // in separate loops: /////////////////////////////////////// if (method === 3) { // MEASURE: Fill the divWidthArr[] with each DIV's clientWidth: divArr.forEach(function (div) { divWidthArr.push(div.clientWidth); }); // MUTATE: Set each DIV's width to twice its clientWidth: divArr.forEach(function (div, index) { div.style.width = (divWidthArr[index] * 2) + 'px'; }); } //////////////////////////////////////// // METHOD 4: no forced reflows. // Measure and mutate operations placed // in separate loops, both in // callback to requestAnimationFrame(): //////////////////////////////////////// if (method === 4) { window.requestAnimationFrame(function () { // MEASURE: Fill the divWidthArr[] with each DIV's clientWidth: divArr.forEach(function (div) { divWidthArr.push(div.clientWidth); }); // MUTATE: Set each DIV's width to twice its clientWidth: divArr.forEach(function (div, index) { div.style.width = (divWidthArr[index] * 2) + 'px'; }); }); } /////////////////////////////// // METHOD 5: no forced reflows. // Distinguish measure operations from mutation operations, // nesting fastdom.mutate within fastdom.measure /////////////////////////////// if (method === 5) { // MEASURE: window.fastdom.measure(function () { divArr.forEach(function (div) { divWidthArr.push(div.clientWidth); }); // MUTATE: window.fastdom.mutate(function () { divArr.forEach(function (div, index) { div.style.width = (divWidthArr[index] * 2) + 'px'; }); }); }); } } // Add click listeners to buttons; call makeList() with button value as argument: Array.prototype.forEach.call(document.querySelectorAll('button'), function (button) { button.addEventListener('click', function () { // Wrap makeList call in rAF: window.requestAnimationFrame(function () { makeList(parseInt(button.value)); }); }); }); }()); </script> </body> </html>
Open the Code in Chrome
If you open forced-reflow-solutions.html in Chrome incognito mode with the DevTools open to the Console display, it should look like this:
Update: In the Console
display, besides Filter
in the menu at the top, click the All Levels
drop-down list and ensure that Verbose
, Info
, Warnings
, and Errors
are all selected:
Press a button and the JavaScript will generate a list of random strings, each in a separate <div>
element, and each surrounded by a colored border that is twice the width of the text the <div>
element contains. The list is headed by a short description of what distinguishes the method employed from the others.
To preview the application and the messages that appear in the DevTools Console display, press each button once and see the results. We’ll examine each routine and analyze its performance in the sections that follow.
Surveying the Methods
Press in the application. After the list is generated, there should be two errors reported in the console:
[Violation] 'requestAnimationFrame' handler took 470ms
[Violation] Forced reflow while executing JavaScript took 434ms
The timing reported should be different, but the error messages should be the same: a requestAnimationFrame
handler error and a forced reflow error.
Now press in the application. After the list is generated, there should be two errors reported in the console:
[Violation] 'requestAnimationFrame' handler took 738ms
[Violation] Forced reflow while executing JavaScript took 703ms
The timing should be longer than Method 1. Now press :
[Violation] 'requestAnimationFrame' handler took 59ms
[Violation] Forced reflow while executing JavaScript took 41ms
The execution time here is much shorter, which is better. Now press :
...nothing! DevTools reports no errors, and the display change was virtually instantaneous. Now press :
...nothing! Again, DevTools reports no errors, and the display change was virtually instantaneous.
We’ll now analyze each of these methods individually, seek the cause of the performance problems, and see the solutions that you can adopt in your own routines. We’ll record the performance of each routine using Chrome’s DevTools.
Running and Recording Method 1
Click on the Performance tab and the DevTools display should change to this:
Next:
- Start recording by clicking the black record button in the DevTools display
- Press in the application
- Press the blue Stop button in the DevTools display as soon as the text list has changed in the browser
The DevTools display will look like this:
The red line in the FPS (Frames Per Second) display means trouble. Click any of the thin blue and pink slices at the bottom of the Main section, and magnify the display by pressing W and A or D. The keyboard controls for adjusting the magnification of the Main section are:
Key | Function |
---|---|
W | to expand the display (“widen” the area) |
S | to shrink the display (“shrink” the area) |
A | to move the display area left |
D | to move the display area right |
Update: As of Chrome 66, released April 2018, you may move, expand, and contract the targeted display area simply by selecting the grey, vertical handle bars on the top left and right of the frame “window,” which is much easier to perform than the QWERTY method described above.
Once the magnification is sufficient, the formerly pinkish slices at the bottom of Main will be distinct purple rectangles, each with a red triangle in the upper righthand corner. Purple is the color for layout in the display, and a red triangle is the flag for a rendering issue. If you hover your cursor over the purple area, the location of the offending code will be identified:
It’s line 139 in forced-synchronous-layout.html, which contains the mixture of measurement (.clientWidth
) and mutation (.width
) in the same statement:
Line Numbers | Code |
---|---|
131 132 133 134 135 136 137 138 139 140 141 152 | /////////////////////////////////////////// // METHOD 1: forced reflows. // Measure (clientWidth) and mutate (width) // in same statement; also performed once // per loop iteration: /////////////////////////////////////////// if (method === 1) { listDiv.appendChild(div); div.style.width = (div.clientWidth * 2) + 'px'; } else { fragment.appendChild(div); } |
Line Numbers | Code |
---|---|
131 132 133 134 135 136 137 138 139 140 141 152 | /////////////////////////////////////////// // METHOD 1: forced reflows. // Measure (clientWidth) and mutate (width) // in same statement; also performed once // per loop iteration: /////////////////////////////////////////// if (method === 1) { listDiv.appendChild(div); div.style.width = (div.clientWidth * 2) + 'px'; } else { fragment.appendChild(div); } |
This is precisely the kind of coding that would be considered efficient or perhaps even elegant in other languages and contexts. Here, it is not performant because of the DOM-thrashing that it causes: the repetitions of measurement and mutation in each iteration of the forEach
loop produce the 1,000 forced reflows, corresponding to the 1,000 purple rectangles with red warning triangle flags in the performance display. (This is the kind of identification procedure that you’ll use to identify the offending code in your own routines when a forced reflow is detected by Chrome.)
Now let’s repeat this procedure, but this time let’s see if Method 2 is any better.
Running and Recording Method 2
Press the Clear button in DevTools to erase the Performance display, then follow the same recording procedure as before, this time selecting :
- Start recording by clicking the black record button in the DevTools display
- Press in the application
- Press the blue Stop button in the DevTools display as soon as the text list has changed in the browser
The results this time are no better:
We have 1,000 individual thrashed layouts again. Here, it’s line 159 in forced-synchronous-layout.html, which again contains the mixture of measurement and mutation on the same line, this time moved to a separate loop outside the <div>
element creation loop:
Line Numbers | Code |
---|---|
151 152 153 154 155 156 157 158 159 160 161 | ///////////////////////////////////////// // METHOD 2: forced reflows. // SLOW. // Measure and mutate in dedicated loop, // but still performed in same statement: ///////////////////////////////////////// if (method === 2) { divArr.forEach(function (div) { div.style.width = (div.clientWidth * 2) + 'px'; }); } |
Line Numbers | Code |
---|---|
151 152 153 154 155 156 157 158 159 160 161 | ///////////////////////////////////////// // METHOD 2: forced reflows. // SLOW. // Measure and mutate in dedicated loop, // but still performed in same statement: ///////////////////////////////////////// if (method === 2) { divArr.forEach(function (div) { div.style.width = (div.clientWidth * 2) + 'px'; }); } |
Simply moving that code outside the <div>
creation loop was not sufficient to cure the forced reflow; it’s the mixing of measurement and mutation that’s the culprit.
Running and Recording Method 3
Now let’s see how well the separation of measurement and mutation performs. Press the Clear button in DevTools to erase the Performance display, then follow the same recording procedure as before, this time selecting . The results this time are better:
The execution time is much shorter, and the 1,000 individual purple layout rectangles with red triangles have been replaced with just 2. Click on the purple rectangle at the bottom and use the resize controls (W, S, A, D) to enlarge it until it looks more or less like this:
Hovering over the purple rectangle reveals that line 173 in the code is problem source:
Line Numbers | Code |
---|---|
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | /////////////////////////////////////// // METHOD 3: occasional forced reflows. // FASTER. // Measure and mutate operations placed // in separate loops: /////////////////////////////////////// if (method === 3) { // MEASURE: Fill the divWidthArr[] with each DIV's clientWidth: divArr.forEach(function (div) { divWidthArr.push(div.clientWidth); }); // MUTATE: Set each DIV's width to twice its clientWidth: divArr.forEach(function (div, index) { div.style.width = (divWidthArr[index] * 2) + 'px'; }); } |
Line Numbers | Code |
---|---|
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | /////////////////////////////////////// // METHOD 3: occasional forced reflows. // FASTER. // Measure and mutate operations placed // in separate loops: /////////////////////////////////////// if (method === 3) { // MEASURE: Fill the divWidthArr[] with each DIV's clientWidth: divArr.forEach(function (div) { divWidthArr.push(div.clientWidth); }); // MUTATE: Set each DIV's width to twice its clientWidth: divArr.forEach(function (div, index) { div.style.width = (divWidthArr[index] * 2) + 'px'; }); } |
Getting .clientWidth
produces a single forced reflow, not 1,000 forced reflows. This is because the measurement is likely performed in between animation frames.
Running and Recording Method 4
Let’s now see the results when we ensure this routine occurs at the beginning of an animation frame. Press the Clear button in DevTools to erase the Performance display, then follow the same recording procedure as before, this time selecting . The results this time are much better:
The purple layout rectangles lack red warning triangles, so Method 4 eliminates the forced layouts. Hovering over the purple rectangles again reveals the corresponding section of JavaScript, and this time the relevant code is line 196:
Line Numbers | Code |
---|---|
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | //////////////////////////////////////// // METHOD 4: no forced reflows. // Measure and mutate operations placed // in separate loops, both in // callback to requestAnimationFrame(): //////////////////////////////////////// if (method === 4) { window.requestAnimationFrame(function () { // MEASURE: Fill the divWidthArr[] with each DIV's clientWidth: divArr.forEach(function (div) { divWidthArr.push(div.clientWidth); }); // MUTATE: Set each DIV's width to twice its clientWidth: divArr.forEach(function (div, index) { div.style.width = (divWidthArr[index] * 2) + 'px'; }); }); } |
Line Numbers | Code |
---|---|
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | //////////////////////////////////////// // METHOD 4: no forced reflows. // Measure and mutate operations placed // in separate loops, both in // callback to requestAnimationFrame(): //////////////////////////////////////// if (method === 4) { window.requestAnimationFrame(function () { // MEASURE: Fill the divWidthArr[] with each DIV's clientWidth: divArr.forEach(function (div) { divWidthArr.push(div.clientWidth); }); // MUTATE: Set each DIV's width to twice its clientWidth: divArr.forEach(function (div, index) { div.style.width = (divWidthArr[index] * 2) + 'px'; }); }); } |
We’ve wrapped the forEach loops of Method 3 within a requestAnimationFrame
request, ensuring that the loops begin at the start of an animation frame. This is the key to preventing forced layouts. But we can take this one step further with Method 5, which uses fastdom.
Running and Recording Method 5
Let’s perform the same procedure using fastdom, the JavaScript function that provides precise control over the queueing of measurements and mutations. (Fastdom.min.js is called from a CDN in a <script>
element located above the site script in the HTML.) Press the Clear button in DevTools to erase the Performance display, then follow the same recording procedure as before, this time selecting . The results this time are the same as Method 4:
The difference between Method 4 and Method 5 is that the latter uses the third-party fastdom JavaScript function. The corresponding code is:
Line Numbers | Code |
---|---|
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | /////////////////////////////// // METHOD 5: no forced reflows. // Measure operations placed within fastdom measure function; // mutate operations placed within nested fastdom mutate function. /////////////////////////////// if (method === 5) { // MEASURE: window.fastdom.measure(function () { divArr.forEach(function (div) { divWidthArr.push(div.clientWidth); }); // MUTATE: window.fastdom.mutate(function () { divArr.forEach(function (div, index) { div.style.width = (divWidthArr[index] * 2) + 'px'; }); }); }); } |
Line Numbers | Code |
---|---|
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | /////////////////////////////// // METHOD 5: no forced reflows. // Measure operations placed within fastdom measure function; // mutate operations placed within nested fastdom mutate function. /////////////////////////////// if (method === 5) { // MEASURE: window.fastdom.measure(function () { divArr.forEach(function (div) { divWidthArr.push(div.clientWidth); }); // MUTATE: window.fastdom.mutate(function () { divArr.forEach(function (div, index) { div.style.width = (divWidthArr[index] * 2) + 'px'; }); }); }); } |
Here, the mutation block is nested inside the measurement block, ensuring that the mutations are cued after the measurements.
Review
We’ve covered how to use the Chrome DevTools to identify the source of forced reflows, and how to restructure JavaScript to prevent them. In general:
- Identify and separate measurement and mutation functions
- Place measurements before mutations in your code
- Use
requestAnimationFrame()
or fastdom to ensure that these procedures occur at the beginnings of animation frames, thus preventing DOM thrashing, a.k.a. forced reflows.
27 September 2017
Last Updated: 24 April 2018