Look at this example:
const startTime = performance.now(); setTimeout(() => console.log(`taken time: ${performance.now() - startTime}ms`)) for(let i = 0; i < 1000; i++){ const element = document.createElement("div"); document.body.appendChild(element); element.textContent = `Текст n${i}`; window.getComputedStyle(document.body); }
and look at another one:
const startTime = performance.now(); setTimeout(() => console.log(`taken time: ${performance.now() - startTime}ms`)) for(let i = 0; i < 1000; i++){ const element = document.createElement("div"); document.body.appendChild(element); element.textContent = `Текст n${i}`; window.getComputedStyle(document.body).width; }
Differences are minimal: in the first case I just invoke window.getComputedStyle(document.body)
without getting property, and in the second case I doing it with width
property. As a result in first one we don’t see recalculation styles and reflows but in the second one we see vesa versa situation. Why?
Advertisement
Answer
This is because getComputedStyle(element)
actually returns a live object.
const el = document.querySelector("div"); const style = getComputedStyle(el); console.log("original color:", style.color); el.classList.add("colored"); // we use the same 'style' object as in the first step console.log("with class color:", style.color);
.colored { color: orange; }
<div>hello</div>
Getting this object doesn’t require that a full recalc is performed, only its getters
will force it.
But browsers are nowadays even smarter than just that, they won’t even trigger a reflow for some properties outside of the CSSOM tree that affects the CSSStyleDeclaration object being inspected.
For instance in the following example we can see that getting the fontSize
property from the CSSStyleDeclaration of an element that is inside our checker will force a reflow affecting our checker, while getting one form outside won’t, because unlike width
, the fontSize
property is only affected by ancestors and not by siblings.
function testReflow(func) { return new Promise( (res, rej) => { const elem = document.querySelector(".reflow-tester"); // set "intermediary" values elem.style.opacity = 1; elem.style.transition = "none"; try { func(elem); } catch(err) { rej(err) } elem.style.opacity = 0; elem.style.transition = "opacity 0.01s"; // if the tested func does trigger a reflow // the transition will start from 1 to 0 // otherwise it won't happen (from 0 to 0) elem.addEventListener("transitionstart", (evt) => { res(true); // let the caller know the result }, { once: true }); // if the transition didn't start in 100ms, it didn't cause a reflow setTimeout(() => res(false), 100); }); } (async () => { await new Promise(res=>setTimeout(res, 1000)); let styles; const gCS_recalc_inner = await testReflow(() => { return (styles = getComputedStyle(document.querySelector("#inner"))); }); console.log("getComputedStyle inner recalc:", gCS_recalc_inner); const gCS_inner_prop_recalc = await testReflow(() => { return styles.fontSize; }); console.log("getComputedStyle inner getter recalc:", gCS_inner_prop_recalc); const gCS_recalc_outer = await testReflow(() => { return (styles = getComputedStyle(document.querySelector("#outer"))); }); console.log("getComputedStyle outer recalc:", gCS_recalc_outer); const gCS_outer_prop_recalc = await testReflow(() => { return styles.fontSize; }); console.log("getComputedStyle outer getter recalc:", gCS_outer_prop_recalc); })().catch(console.error);
.reflow-tester { opacity: 0; } .hidden { display: none; }
<div class="reflow-tester">Tester<div id="inner"></div></div> <div id="outer"></div>
The same checking for width
would trigger in both cases, because width
can be affected by siblings:
function testReflow(func) { return new Promise( (res, rej) => { const elem = document.querySelector(".reflow-tester"); // set "intermediary" values elem.style.opacity = 1; elem.style.transition = "none"; try { func(elem); } catch(err) { rej(err) } elem.style.opacity = 0; elem.style.transition = "opacity 0.01s"; // if the tested func does trigger a reflow // the transition will start from 1 to 0 // otherwise it won't happen (from 0 to 0) elem.addEventListener("transitionstart", (evt) => { res(true); // let the caller know the result }, { once: true }); // if the transition didn't start in 100ms, it didn't cause a reflow setTimeout(() => res(false), 100); }); } (async () => { await new Promise(res=>setTimeout(res, 1000)); let styles; const gCS_recalc_inner = await testReflow(() => { return (styles = getComputedStyle(document.querySelector("#inner"))); }); console.log("getComputedStyle inner recalc:", gCS_recalc_inner); const gCS_inner_prop_recalc = await testReflow(() => { return styles.width; }); console.log("getComputedStyle inner getter recalc:", gCS_inner_prop_recalc); const gCS_recalc_outer = await testReflow(() => { return (styles = getComputedStyle(document.querySelector("#outer"))); }); console.log("getComputedStyle outer recalc:", gCS_recalc_outer); const gCS_outer_prop_recalc = await testReflow(() => { return styles.width; }); console.log("getComputedStyle outer getter recalc:", gCS_outer_prop_recalc); })().catch(console.error);
.reflow-tester { opacity: 0; } .hidden { display: none; }
<div class="reflow-tester">Tester<div id="inner"></div></div> <div id="outer"></div>