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);
}
@chestozo
Copy link

chestozo commented Jan 20, 2017

@Krinkle it your version Date.now() inside cb will be the same as start.
Whereas here you have access to the start time before any idle callbacks were executed.

Basically in original version you can kind of limit idle period
whereas in your version you can only check execution time of individual idle callbacks:

https://developers.google.com/web/updates/2015/08/using-requestidlecallback

@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