jsPerf.app is an online JavaScript performance benchmark test runner & jsperf.com mirror. It is a complete rewrite in homage to the once excellent jsperf.com now with hopefully a more modern & maintainable codebase.
jsperf.com URLs are mirrored at the same path, e.g:
https://jsperf.com/negative-modulo/2
Can be accessed at:
https://jsperf.app/negative-modulo/2
Measures total ms needed to:
① capture first state ② apply new grid layout ③ prepare/invert transforms, not the play phase (animating is compositor work).
<!-- libs -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/Flip.min.js"></script>
<style>
/* ——— base ——— */
body{
margin:0;
font-family:sans-serif;
background:#111;
color:#ccc;
}
.stage{
display:grid;
grid-template-columns:repeat(auto-fill,70px);
gap:8px; padding:20px;
/* container-query setup */
container-type:inline-size;
contain:layout paint style;
width:400px;
}
.card{
width:70px;height:70px;
background:#3a79ff;
border-radius:6px;
contain:layout paint style;
}
/* ——— container query overrides ——— */
@container (min-width:500px){
.card{
width:120px;
height:120px;
background:#ff5d3a; /* visual proof CQ fired */
}
}
</style>
<div id="stage" class="stage"></div>// ---------- one-time setup ----------
const CARD_COUNT = 400;
const stage = document.getElementById('stage');
const rectPool = new Array(CARD_COUNT); // object-pool
const io = new IntersectionObserver(() => {}, { root: stage });
// fill pool with empty rect-like objects
for (let i = 0; i < CARD_COUNT; i++) {
rectPool[i] = {};
}
// helper – register every child exactly once
function registerCards() {
[...stage.children].forEach((el) => io.observe(el));
}
// ---------- functions reused in multiple tests ----------
function makeGrid(reverse = false) {
stage.innerHTML = '';
const f = document.createDocumentFragment();
for (let i = 0; i < CARD_COUNT; i++) {
const d = document.createElement('div');
d.className = 'card';
d.dataset.id = reverse ? CARD_COUNT - i : i;
f.appendChild(d);
}
stage.appendChild(f);
registerCards(); // (re)attach IO observers
}
// Tick-bound memoized rect helper
let rectCacheTB = new WeakMap();
let purge = false;
function rectTB(el) {
let r = rectCacheTB.get(el);
if (!r) {
r = el.getBoundingClientRect();
rectCacheTB.set(el, r);
if (!purge) {
purge = true;
queueMicrotask(() => {
rectCacheTB = new WeakMap();
purge = false;
});
}
}
return r;
}
// ------------ helpers shared by tests ----
const rectCache = new WeakMap(); // element -> DOMRect
const matCache = new WeakMap(); // element -> {dx,dy,sx,sy}
function readAllRects() {
const els = stage.children;
const out = new Array(els.length);
for (let i = 0; i < els.length; i++) {
out[i] = els[i].getBoundingClientRect();
}
return out;
}
function getAllRectsViaIO() {
// takeRecords() is cheap and returns *all* pending entries synchronously
const entries = io.takeRecords();
const len = entries.length;
for (let i = 0; i < len; i++) {
const r = entries[i].boundingClientRect; // already up-to-date
// write into the pre-allocated pool object (no new allocations)
const o = rectPool[i];
o.left = r.left;
o.top = r.top;
o.width = r.width;
o.height = r.height;
}
return rectPool;
}
function computeFrames(a, b) {
const out = new Array(a.length);
for (let i = 0; i < a.length; i++) {
const ai = a[i],
bi = b[i];
out[i] = {
dx: ai.left - bi.left,
dy: ai.top - bi.top,
sx: ai.width / bi.width,
sy: ai.height / bi.height,
};
}
return out;
}
function writeAllTransforms(frames) {
const els = stage.children;
for (let i = 0; i < els.length; i++) {
const t = frames[i];
els[i].style.transform =
`translate(${t.dx}px,${t.dy}px) scale(${t.sx},${t.sy})`;
}
}
// keep track of CQ state so we can skip work when nothing changes
let isWide = false;
/***************** string-pool for transforms ******************/
const txPool = Object.create(null); // key => string
function pooledTransform(dx, dy, sx, sy) {
// dx|dy|sx|sy is < 12 chars, fast key
const k = dx + '|' + dy + '|' + sx + '|' + sy;
return (
txPool[k] || (txPool[k] = `translate(${dx}px,${dy}px) scale(${sx},${sy})`)
);
}
/***************** IO with jumbo rootMargin ******************/
const ioRM = new IntersectionObserver(
() => {}, // we pull via takeRecords()
{ root: stage, rootMargin: '100% 0px 100% 0px' },
);
function registerCardsRM() {
[...stage.children].forEach((el) => ioRM.observe(el));
}
function getRectsViaIORM() {
const entries = ioRM.takeRecords(); // all in one go
for (let i = 0; i < entries.length; i++) {
const r = entries[i].boundingClientRect;
const o = rectPool[i]; // object-pool reuse
o.left = r.left;
o.top = r.top;
o.width = r.width;
o.height = r.height;
}
return rectPool;
}
Ready to run.
| Test | Ops/sec | |
|---|---|---|
| Naïve FLIP (per-node read) | | ready |
| Batched FLIP (single read, single write) | | ready |
| Cached FLIP (skip unchanged) | | ready |
| GSAP Flip (v7) | | ready |
| GSAP Flip (v7) + Container-Query | | ready |
| Batched FLIP with tick-bound memo | | ready |
| Batched FLIP + pool + CQ guard + IO | | ready |
| Pure IntersectionObserver + Pool | | ready |
| Batched FLIP + pooled transform strings | | ready |
| Transform-string pool + CSS Typed OM styleMap | | ready |
| Skip IntersectionObserver during requestAnimationFrame drag | | ready |
You can edit these tests or add more tests to this page by appending /edit to the URL.