Skip to content

Instantly share code, notes, and snippets.

@paullewis
Last active February 11, 2026 04:23
Show Gist options
  • Select an option

  • Save paullewis/55efe5d6f05434a96c36 to your computer and use it in GitHub Desktop.

Select an option

Save paullewis/55efe5d6f05434a96c36 to your computer and use it in GitHub Desktop.
Shims rIC in case a browser doesn't support it.
/*!
* Copyright 2015 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
/*
* @see https://developers.google.com/web/updates/2015/08/using-requestidlecallback
*/
window.requestIdleCallback = window.requestIdleCallback ||
function (cb) {
return setTimeout(function () {
var start = Date.now();
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
}
window.cancelIdleCallback = window.cancelIdleCallback ||
function (id) {
clearTimeout(id);
}
@johnzhou721
Copy link

Alirght... seems that using requestAnimationFrame we're able to do a little better, because we can actually simulate an idle cycle and then cap the amount of time we do work in it. With this approximate polyfill I threw together, the framerate of a fairly complex OpenGL animation doesn't drop at all with a splittable task running:


// Internal state
globalThis.__idleRequestCallbacks = [];
globalThis.__runnableIdleCallbacks = [];
globalThis.__idleCallbackId = 0;
globalThis.__idleCallbackMap = new Map();
globalThis.__idleRafScheduled = false;

// IdleDeadline constructor
function IdleDeadline(deadlineTime, didTimeout) {
  this.__deadlineTime = deadlineTime;
  this.__didTimeout = didTimeout;
}
IdleDeadline.prototype.timeRemaining = function() {
  var remaining = this.__deadlineTime - performance.now();
  return remaining > 0 ? remaining : 0;
};
Object.defineProperty(IdleDeadline.prototype, 'didTimeout', {
  get: function() { return this.__didTimeout; },
});

  function scheduleNextIdle() {
    if (globalThis.__idleRafScheduled) return;
    globalThis.__idleRafScheduled = true;

    requestAnimationFrame(() => {
      setTimeout(startIdlePeriod, 0);
    });
  }

// Start an idle period
function startIdlePeriod() {
  // console.log("before animaiton frame")
  // Move pending to runnable
  if (globalThis.__idleRequestCallbacks.length) {
    globalThis.__runnableIdleCallbacks.push(...globalThis.__idleRequestCallbacks);
    globalThis.__idleRequestCallbacks.length = 0;
  }

  globalThis.__idleRafScheduled = false; // reset flag

  // If no runnable callbacks or already scheduled, exit
  if (!globalThis.__runnableIdleCallbacks.length) return;

  var deadlineTime = performance.now() + 8; // 8 does not drop framerate on most modern systems

  while (globalThis.__runnableIdleCallbacks.length) {
    var handle = globalThis.__runnableIdleCallbacks.shift();
    var cb = globalThis.__idleCallbackMap.get(handle);
    if (!cb) continue; // canceled
    // Cancel this, so we no longer call it on timeout
    globalThis.__idleCallbackMap.delete(handle);

    var deadline = new IdleDeadline(deadlineTime, false);
    try { cb(deadline); } catch (e) { setTimeout(() => { throw e; }, 0); }

    if (performance.now() >= deadlineTime) break; // yield mid-frame
  }

  // Reschedule if any callbacks remain
  if (globalThis.__runnableIdleCallbacks.length) {
    scheduleNextIdle();
  }

}

function requestIdleCallback(callback, options) {
    var handle = ++globalThis.__idleCallbackId;
    globalThis.__idleCallbackMap.set(handle, callback);
    globalThis.__idleRequestCallbacks.push(handle);

    if (options && options.timeout && options.timeout > 0) {
      // FIXME: Spec says that the timeout calling must sort by currentTime +
      // options.timeout, however maintainng such a queue would be very dedious
      setTimeout(function timeoutCallback() {
        var cb = globalThis.__idleCallbackMap.get(handle);
        if (!cb) return;
        var i = globalThis.__idleRequestCallbacks.indexOf(handle);
        if (i > -1) globalThis.__idleRequestCallbacks.splice(i, 1);
        i = globalThis.__runnableIdleCallbacks.indexOf(handle);
        if (i > -1) globalThis.__runnableIdleCallbacks.splice(i, 1);
        var deadline = new IdleDeadline(performance.now(), true);
        try { cb(deadline); } catch (e) { setTimeout(() => { throw e; }, 0); }
      }, options.timeout);
    }

    scheduleNextIdle();
    return handle;
  }

  function cancelIdleCallback(handle) {
    globalThis.__idleCallbackMap.delete(handle);
    var i = globalThis.__idleRequestCallbacks.indexOf(handle);
    if (i > -1) globalThis.__idleRequestCallbacks.splice(i, 1);
    i = globalThis.__runnableIdleCallbacks.indexOf(handle);
    if (i > -1) globalThis.__runnableIdleCallbacks.splice(i, 1);
  }

I'm using an OpenGL animation generated by ChatGPT:

Animation
(function GPUStressDemo() {
  'use strict';

  // --- utility: create element with styles ---
  function el(tag, style) {
    const e = document.createElement(tag);
    Object.assign(e.style, style || {});
    return e;
  }

  // --- create canvas & UI (no HTML required) ---
  const canvas = el('canvas', {
    position: 'fixed',
    inset: '0', // top/right/bottom/left 0
    width: '100%',
    height: '100%',
    zIndex: 2147483647, // topmost (very high)
    display: 'block',
    cursor: 'default',
    pointerEvents: 'none', // so it doesn't block interaction
    background: 'black',
  });
  document.body.appendChild(canvas);

  const ui = el('div', {
    position: 'fixed',
    top: '8px',
    left: '8px',
    zIndex: 2147483647,
    padding: '6px 10px',
    borderRadius: '6px',
    background: 'rgba(0,0,0,0.6)',
    color: '#0f0',
    fontFamily: 'monospace',
    fontSize: '13px',
    lineHeight: '1.2',
    pointerEvents: 'auto', // allow button clicks
  });
  document.body.appendChild(ui);

  const fpsEl = el('div');
  const infoEl = el('div', { marginTop: '6px', color: '#9f9' });
  const stopBtn = el('button', {
    marginTop: '6px',
    padding: '4px 8px',
    cursor: 'pointer',
    borderRadius: '4px',
    border: '1px solid rgba(255,255,255,0.12)',
    background: 'rgba(255,255,255,0.02)',
    color: '#fff',
    fontSize: '12px'
  });
  stopBtn.textContent = 'Stop Demo';
  ui.appendChild(fpsEl);
  ui.appendChild(infoEl);
  ui.appendChild(stopBtn);

  // --- WebGL setup ---
  const gl = canvas.getContext('webgl', { antialias: false, preserveDrawingBuffer: false }) ||
             canvas.getContext('experimental-webgl', { antialias: false });

  if (!gl) {
    fpsEl.textContent = 'WebGL not available in this browser.';
    return;
  }

  // Vertex shader (simple full-screen triangle)
  const vertSrc = `
    attribute vec2 a_position;
    varying vec2 v_uv;
    void main() {
      v_uv = a_position * 0.5 + 0.5;
      gl_Position = vec4(a_position, 0.0, 1.0);
    }
  `;

  // Fragment shader: heavy per-pixel iterative computation (GPU-hungry)
  const fragSrc = `
    precision highp float;
    varying vec2 v_uv;
    uniform vec2 u_resolution;
    uniform float u_time;

    // A tiny hash for variety
    float hashf(float n){ return fract(sin(n)*43758.5453123); }

    void main(){
      vec2 uv = (gl_FragCoord.xy / u_resolution.xy) * 2.0 - 1.0;
      uv.x *= u_resolution.x / u_resolution.y;

      // center + zoom animation
      float t = u_time * 0.35;
      vec2 z = uv * 1.25;
      vec3 col = vec3(0.0);

      // Heavy iterative loop: 200 iterations (constant loop, expensive ops)
      // This is intentionally costly to expose GPU lag.
      for(int k = 0; k < 200; k++) {
        float kk = float(k) + 0.5;
        float a = kk * 0.141592 + t * 0.15;
        // complex transform mixing trig and multiplication
        vec2 z2 = vec2(
          z.x*z.x - z.y*z.y,
          2.0*z.x*z.y
        );
        // twist with sin/cos and an evolving offset
        z = 0.95 * z2 + 0.3 * vec2(sin(a + z2.x*0.7), cos(a*0.87 + z2.y*0.9));

        // energy-like accumulator: uses length and multiple trig calls
        float m = length(z) + 0.0001;
        float s = 1.0 / (0.0005 + m*m*0.7);
        col += s * vec3( sin(kk*0.123 + t*0.4),
                        cos(kk*0.117 + t*0.23),
                        sin(kk*0.197 - t*0.31) );
      }

      // tone map, saturation
      col = col * 0.01;
      col = pow(clamp(col, 0.0, 1.0), vec3(0.9));
      gl_FragColor = vec4(col, 1.0);
    }
  `;

  function compileShader(source, type) {
    const s = gl.createShader(type);
    gl.shaderSource(s, source);
    gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
      const msg = gl.getShaderInfoLog(s);
      gl.deleteShader(s);
      throw new Error('Shader compile error: ' + msg);
    }
    return s;
  }

  function createProgram(vsSrc, fsSrc) {
    const vs = compileShader(vsSrc, gl.VERTEX_SHADER);
    const fs = compileShader(fsSrc, gl.FRAGMENT_SHADER);
    const prog = gl.createProgram();
    gl.attachShader(prog, vs);
    gl.attachShader(prog, fs);
    gl.linkProgram(prog);
    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
      const msg = gl.getProgramInfoLog(prog);
      gl.deleteProgram(prog);
      throw new Error('Program link error: ' + msg);
    }
    gl.deleteShader(vs);
    gl.deleteShader(fs);
    return prog;
  }

  const program = createProgram(vertSrc, fragSrc);
  gl.useProgram(program);

  // a_position buffer for a fullscreen triangle (two triangles via strip)
  const quad = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, quad);
  // full-screen triangle strip coords
  const vertices = new Float32Array([
    -1, -1,
     1, -1,
    -1,  1,
     1,  1
  ]);
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

  const aPos = gl.getAttribLocation(program, 'a_position');
  gl.enableVertexAttribArray(aPos);
  gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

  const uResolution = gl.getUniformLocation(program, 'u_resolution');
  const uTime = gl.getUniformLocation(program, 'u_time');

  // Resize handling with devicePixelRatio cap to avoid blindingly huge canvases
  function resize() {
    const dpr = Math.min(window.devicePixelRatio || 1, 2.0); // cap DPR at 2 for sanity
    const width = Math.max(1, Math.round(window.innerWidth * dpr));
    const height = Math.max(1, Math.round(window.innerHeight * dpr));
    if (canvas.width !== width || canvas.height !== height) {
      canvas.width = width;
      canvas.height = height;
      canvas.style.width = window.innerWidth + 'px';
      canvas.style.height = window.innerHeight + 'px';
      gl.viewport(0, 0, width, height);
    }
  }
  window.addEventListener('resize', resize, { passive: true });
  resize();

  // --- animation loop with FPS tracking ---
  let running = true;
  let last = performance.now();
  let fps = 0;
  let frames = 0;
  let acc = 0;
  let startTime = performance.now();

  function updateFPS(delta) {
    frames++;
    acc += delta;
    if (acc >= 250) { // update 4x/sec for a responsive readout
      fps = Math.round((frames / acc) * 1000);
      frames = 0;
      acc = 0;
      fpsEl.textContent = `FPS: ${fps}`;
      infoEl.textContent = `Resolution: ${canvas.width}×${canvas.height} (dpr capped)`;
    }
  }

  function frame(now) {
    if (!running) return;
    const dt = now - last;
    last = now;
    updateFPS(dt);

    // Send uniforms
    gl.uniform2f(uResolution, canvas.width, canvas.height);
    gl.uniform1f(uTime, (now - startTime) * 0.001);

    // Render full-screen quad (triangle strip of 4 verts)
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

    requestAnimationFrame(frame);
  }

  // --- controls ---
  stopBtn.addEventListener('click', () => stop(), { once: true });

  function stop() {
    running = false;
    // remove elements and event listeners
    window.removeEventListener('resize', resize);
    try { gl.getExtension('WEBGL_lose_context')?.loseContext(); } catch (e) {}
    if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
    if (ui.parentNode) ui.parentNode.removeChild(ui);
  }

  // Start loop
  requestAnimationFrame((t) => {
    last = t;
    startTime = t;
    requestAnimationFrame(frame);
  });

  // Expose stop to window for convenience
  window.__gpuStressStop = stop;

  // Friendly message to console
  console.log('GPU stress demo started. Call window.__gpuStressStop() to stop.');

  // return stop in case someone wants to capture it
  return { stop };
})();

And this code for the computation running on top of it:

const N = 8000;

let i = 1;
let j = 1;
let total = 0;

function work(deadline) {
  while (i <= N) {
    total += i * j;

    j++;
    if (j > i) {
      i++;
      j = 1;
    }

    if (deadline.timeRemaining() < 1) {
      requestIdleCallback(work);
      return;
    }
  }

  console.log("Done:", total);
}

requestIdleCallback(work);

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