Skip to content

Instantly share code, notes, and snippets.

@WebReflection
Last active November 13, 2021 20:52
Show Gist options
  • Save WebReflection/5aaca727bc3b784d43be3704cf65abff to your computer and use it in GitHub Desktop.
Save WebReflection/5aaca727bc3b784d43be3704cf65abff to your computer and use it in GitHub Desktop.
Classes VS DOM Events Handling Benchmark
// Players
class ClickCounter {
constructor() { this.clicks = 0; }
onclick(e) { this.clicks += (e.type === 'click') ? 1 : -1; }
}
class Handler extends ClickCounter {
constructor(currentTarget) {
super();
currentTarget.addEventListener('click', this);
}
}
class DynamicHandler extends Handler {
handleEvent(e) { this['on' + e.type](e); }
}
class StaticHandler extends Handler {
handleEvent(e) { switch (e.type) {
case 'click': return this.onclick(e);
}}
}
// just to rule out hierarchy performance
class Bounder extends ClickCounter {
constructor(currentTarget) { super(); }
}
class ArrowHandler extends Bounder {
constructor(currentTarget) {
super(currentTarget);
this.click = (e) => this.onclick(e);
currentTarget.addEventListener('click', this.click);
}
}
class BoundHandler extends Bounder {
constructor(currentTarget) {
super(currentTarget);
this.onclick = this.onclick.bind(this);
currentTarget.addEventListener('click', this.onclick);
}
}
// Rules
const benchmark = (Class, length = 1000, samples = 5) => {
const currentTarget = button(Class.name);
const instances = new Array(length);
return new Promise(res => setTimeout(res, 500)).then(() => {
let benchName;
let memory;
benchName = `new ${Class.name}(currentTarget)`;
memory = performance.memory.usedJSHeapSize;
console.time(benchName);
for (let i = 0; i < length; i++)
instances[i] = new Class(currentTarget);
console.timeEnd(benchName);
memory = performance.memory.usedJSHeapSize - memory;
if (memory) console.log('memory: ', memory);
const event = new Event('click');
benchName = 'currentTarget.dispatchEvent(clickEvent)';
memory = performance.memory.usedJSHeapSize;
console.time(benchName);
for (let i = 0; i < samples; i++)
currentTarget.dispatchEvent(event);
console.timeEnd(benchName);
memory = performance.memory.usedJSHeapSize - memory;
if (memory) console.log('memory: ', memory);
console.assert(
instances.every(instance => instance.clicks === samples),
`expected ${length} clicks, got ${instances[0].clicks} instead`
);
});
};
// Helpers
const button = textContent => {
const el = document.createElement('button');
el.textContent = textContent;
return document.body.appendChild(el);
};
// Race !
var instances = 10000; // how many instances ?
var dispatches = 10; // how many dispatches ?
Promise
.resolve()
.then(() => benchmark(DynamicHandler, instances, dispatches))
.then(() => benchmark(StaticHandler, instances, dispatches))
.then(() => benchmark(ArrowHandler, instances, dispatches))
.then(() => benchmark(BoundHandler, instances, dispatches))
;
@WebReflection
Copy link
Author

screenshot from 2017-06-17 10-42-18

@WebReflection
Copy link
Author

WebReflection commented Jun 19, 2017

Alternative with perf-monitor

(function (script) {
  script.src = 'https://unpkg.com/[email protected]/dist/umd/perf-monitor.js';
  script.onload = function () {
    perfMonitor.startMemMonitor();
    var instances = 1000;  // how many instances ?
    var dispatches = 10;    // how many dispatches ?
    var all = [
      DynamicHandler,
      StaticHandler,
      ArrowHandler,
      BoundHandler
    ];
    all.forEach(Class => {
      perfMonitor.initProfiler(Class.name);
    });

    (function bench() {
      all.reduce((previus, Class) =>
        previus.then(() => benchmark(Class, instances, dispatches)),
        Promise.resolve()
      ).then(bench);
    }());

  };
}(
  document.head.appendChild(
    document.createElement('script')
  )
));

// Players
class ClickCounter {
  constructor() { this.clicks = 0; }
  onclick(e) { this.clicks += (e.type === 'click') ? 1 : -1; }
}

class Handler extends ClickCounter {
  constructor(currentTarget) {
    super();
    currentTarget.addEventListener('click', this);
  }
}

class DynamicHandler extends Handler {
  handleEvent(e) { this['on' + e.type](e); }
}

class StaticHandler extends Handler {
  handleEvent(e) { switch (e.type) {
    case 'click': return this.onclick(e);
  }}
}

// just to rule out hierarchy performance
class Bounder extends ClickCounter {
  constructor(currentTarget) { super(); }
}

class ArrowHandler extends Bounder {
  constructor(currentTarget) {
    super(currentTarget);
    this.click = (e) => this.onclick(e);
    currentTarget.addEventListener('click', this.click);
  }
}

class BoundHandler extends Bounder {
  constructor(currentTarget) {
    super(currentTarget);
    this.onclick = this.onclick.bind(this);
    currentTarget.addEventListener('click', this.onclick);
  }
}

// Rules
const benchmark = (Class, length = 1000, samples = 5) => {

  const currentTarget = button(Class.name);
  const instances = new Array(length);

  return new Promise(res => {
    requestAnimationFrame(() => {
      perfMonitor.startProfile(Class.name);
      for (let i = 0; i < length; i++)
        instances[i] = new Class(currentTarget);

      const event = new Event('click');
      for (let i = 0; i < samples; i++)
        currentTarget.dispatchEvent(event);
      perfMonitor.endProfile(Class.name);

      console.assert(
        instances.every(instance => instance.clicks === samples),
        `expected ${length} clicks, got ${instances[0].clicks} instead`
      );

      currentTarget.parentNode.removeChild(currentTarget);
      res();

    });
  });

};

// Helpers
const button = textContent => {
  const el = document.createElement('button');
  el.textContent = textContent;
  return document.body.appendChild(el);
};

@JasonRammoray
Copy link

I guess instead of "expected ${length} clicks, got ${instances[0].clicks} instead" we should use "expected ${samples} clicks, got ${instances[0].clicks} instead", aren't we?

@JasonRammoray
Copy link

JasonRammoray commented Mar 12, 2018

Just one more thing.
I launched a code of this benchmark (Chrome Version 64.0.3282.186 (Official Build) (64-bit), MacOS High Sierra) and got some different results, which shows an enormous memory consumption for handleEvent.
@WebReflection, can you, please, comment on that?
screen shot 2018-03-12 at 13 51 17

@JasonRammoray
Copy link

JasonRammoray commented Mar 14, 2018

I've posted a question on StackOverflow.
The answer is pretty simple.
handleEvent consumed that much memory due to issue in the benchmark.
The thing is that strings concatenation ('on' + e.type) causes new string allocation in heap on each iteration.
If we just replace this['on' + e.type] to switch(e.type) {case 'click': this.onclick(e);}, then we get results, which are reflecting initial idea regarding benefits of using handleEvent.
screen shot 2018-03-14 at 13 00 34

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment