Cross-browser Performance Compatibility

...or why testing on Chrome is not enough.

About me

Head of Engineering at 

Author of uniforms

Everything is at radekmie.dev

2023-10-01

Core contributor of Meteor.js

Cross-browser compatibility

Cross-browser compatibility is the ability of a website or web application to function across different browsers and degrade gracefully when browser features are absent or lacking.

~ Wikipedia

Cross-browser compatibility

jQuery

underscore.js

core-js

@babel/preset-env

Cross-browser performance compatibility

???

???

???

???

Case study

// Concat plain strings: 'a'
joinName('a');











 

Most important helper of uniforms




// Flatten and concat strings in arrays: 'b.c'
joinName(['b'], 'c');








 






// Concat numbers: 'd.0'
joinName('d', 0);





 









// Ignore `undefined` and `null`: 'e'
joinName(undefined, 'e');


 












// All of the above: 'a.b.1.c.d.2.e'
joinName('a.b', 1, ['c', 'd'], undefined, 2, 'e');

Naive implementation

function joinName(...parts: unknown[]) {
  return parts
    .reduce(
       (path, part) =>







       [],
     )
    .join('.');
}




         part || part === 0
           ? path.concat(
               typeof part === 'string'
                 ? part.split('.')
                 : part
             )
           : path,




Benchmark #1

for (let round = 1; round <= 50; ++round) {
  console.time();
  for (let _ = 0; _ < 100000; ++_) {
    joinName('a');
    joinName(['b'], 'c');
    joinName('d', 0);
    joinName(undefined, 'e');
  }
  console.timeEnd();
}

Benchmark #1

Browser avg max min std sum
Chrome 117 38.2103ms 68.7729ms 36.6030ms 4.39672ms 1.910515s
Firefox 117 105.560ms 122.000ms 103.000ms 2.69191ms 5.278000s
Safari 16.6 62.7956ms 92.2490ms 60.9030ms 4.26657ms 3.139782s
  1. The performance difference between browsers is massive.
  2. JIT compilation helps, making the avg time significantly lower than max.
  3. Firefox 💀

Better implementation

function joinName(...parts) {
  const path = [];
  for (let index = 0; index !== parts.length; ++index) {

    
    
    
    
    
    
    
    
    
    
    
    
    
  }
  return path.join('.');
}
 


    const part = parts[index];
    if (part || part === 0) {

      
      
      
      
      
      
      
      
      
      
    }


 
 




      if (typeof part === 'string') {
        if (part.indexOf('.') !== -1) {
          path.push(...part.split('.'));
        } else {
          path.push(part);
        }

        
        
        
        

      
      
      
 










      } else if (Array.isArray(part)) {
        parts.splice(index--, 1, ...part);

        
        
        
        
        
        
 












      } else {
        path.push('' + part);
      }



 

Benchmark #2

Browser avg max min std sum
Chrome 117 25.4948ms 53.3352ms 24.3627ms 4.09573ms 1.274742s
Firefox 117 101.340ms 119.000ms 99.0000ms 2.62762ms 5.067000s
Safari 16.6 44.3199ms 72.2010ms 42.6200ms 4.03886ms 2.215998s
  • 33% faster in Chrome
  • 29% faster in Safari
  • 4% faster in Firefox

Release time!

Hey, I've noticed the new implementation is actually significantly slower for me!

~ Someone using Firefox

What!?

Benchmark #3

for (let round = 1; round <= 50; ++round) {
  console.time();
  for (let _ = 0; _ < 100000; ++_) {
    joinName('a');
    joinName(['b'], 'c');
    joinName('d', 0);
    joinName(undefined, 'e');

  }
  console.timeEnd();
}

Benchmark #3

for (let round = 1; round <= 50; ++round) {
  console.time();
  for (let _ = 0; _ < 100000; ++_) {
    joinName('a');
    joinName(['b'], 'c');
    joinName('d', 0);
    joinName(undefined, 'e');

  }
  console.timeEnd();
}

One more real-life scenario

 






    joinName('a.b', 1, ['c', 'd'], null, 2, 'e');


 

Benchmark #3

Browser avg max min std sum
v1 Chrome 117 80.8508ms 113.457ms 78.8718ms 4.66811ms 4.042544s
v1 Firefox 117 158.740ms 175.000ms 155.000ms 3.21751ms 7.937000s
v1 Safari 16.6 101.327ms 125.107ms 98.8790ms 3.61048ms 5.066369s
  • 37% faster in Chrome
  • 17% faster in Safari
  • 14% slower in Firefox
v2 Chrome 117 50.3077ms 83.5910ms 48.7460ms 4.80059ms 2.515386s
v2 Firefox 117 182.060ms 195.000ms 176.000ms 4.56249ms 9.103000s
v2 Safari 16.6 83.8215ms 112.198ms 81.4840ms 4.23691ms 4.191076s

What now?

  1. Engineer something that works better everywhere, not only in some browsers.
  2. Leave it as it is, not to make it worse for Firefox users.
  3. Calculate real-life user impact based on available analytics.

User impact

Chrome

iOS Safari

Safari

Edge

Firefox

Other

9%

8%

Overall, it's faster for 83% and slower for 9% of users.

Remaining 8%
is unchecked!

One more case study

 











// Sample usage:
[omitUnnecessaryFields({arrayIndices, value: undefined})];

We're messing up with hidden classes in V8!

const omitUnnecessaryFields = result => {
  if (!result.dontIterate) {
    delete result.dontIterate;
  }

  if (result.arrayIndices && !result.arrayIndices.length) {
    delete result.arrayIndices;
  }

  return result;
};


 

One more case study

function buildResult(arrayIndices, dontIterate, value) {
  return arrayIndices && arrayIndices.length
    ? dontIterate
      ? [{ arrayIndices, dontIterate, value }]
      : [{ arrayIndices, value }]
    : dontIterate
      ? [{ dontIterate, value }]
      : [{ value }];
}


 
 









// Sample usage:
buildResult(arrayIndices, false, undefined);

V8 is happy now!

Benchmark

Depending on the test case, it got 50%-90% faster in Chrome and between 10% slower or faster on Safari.

>95% of the usage is on the server! Node.js and Chrome use the same V8 runtime, so...

Ship it!

Takeaways

  1. Benchmark every time.
  2. Benchmark real-life scenarios.
  3. Benchmark in multiple runtimes.
  4. Account for your user base.
  5. Remember that there are other browsers, not only Chrome!

Questions?