Skip to content
Advertisement

Why doesn’t window.getComputedStyle invoke recalculate styles and reflow?

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>
User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement