Onigiri the cat
ONIGIRICode Supervisor

PERF LAB

Frontend performance techniques you can drive yourself. The numbers are measured live, not screenshots — change the inputs and they change. More will be added over time.

01useMemo

Caching expensive work

The same calculation (counting primes in a range), with a switch for whether it goes through the useMemo cache, timing the same re-render either way.

Press “force a re-render” — that state has nothing to do with the calculation, yet the right panel recomputes anyway. That is the problem useMemo solves.

Read the write-up

msthis re-render

useMemo ON
useMemo OFF
primes found
4,203
render count
#0
useMemo
02React.lazy

Load as you scroll

The chart component is not in the main bundle — Vite splits it into its own chunk. It is fetched only when you press the button, so the homepage never carries its weight.

Open the Network tab and press it again: a new js file is requested at exactly that moment.

Read the write-up
    scroll inside the box for more
    0 / 60 rows
    03Suspense

    Waiting for data

    When the data is not ready the component throws a promise. React catches it, shows the skeleton, and retries the render once the promise settles. That is the Suspense contract.

    Drag the latency and replay — the skeleton stays up for exactly that long. This page itself is prerendered too: its first-paint HTML is generated at build time.

    Read the write-up

    no request started

    04虛擬捲動

    Render only what is visible

    Both sides hold the same 10,000 rows in memory. The left renders every one into the DOM; the right renders only the ~20 in view and offsets them into place.

    This is the inverse of demo 02: that one fetches less data, this one paints less DOM.

    Read the write-up
    render everything
    not rendered yet (the button will stall the page briefly)
    DOM nodes
    first paint
    virtualised
    #1row-00001
    #2row-00002
    #3row-00003
    #4row-00004
    #5row-00005
    #6row-00006
    #7row-00007
    #8row-00008
    #9row-00009
    #10row-00010
    #11row-00011
    #12row-00012
    DOM nodes
    12
    first paint
    0 ms

    A 500x difference in node count, which shows up directly in scroll smoothness and memory use.

    05Web Worker

    Move heavy work off the main thread

    The same prime calculation, once on the main thread and once in a Web Worker. Identical work, identical result — only the thread differs.

    Watch the spinner. On the main thread it freezes solid; in the worker it never stops. That freeze is what users experience as a hang.

    Read the write-up
    watch the spinner, then press a button
    last run on
    elapsed
    primes found
    06Map vs find

    The cost of looking up inside a loop

    Mapping every order to a user name. One calls find inside the map; the other builds a Map index once and looks up from it. Results are identical and verified against each other.

    Calling find inside map is the most common performance trap in list rendering — one innocent line that costs n x m comparisons. Double the data and the find side gets four times slower while the Map side only doubles.

    Read the write-up
    map + find
    orders.map(o =>
      users.find(
        u => u.id === o.userId
      )
    )
    users is an array, so it scans from the start until it hits a match — once per order.
    Map index first
    const index = new Map(
      users.map(u => [u.id, u])
    )
    orders.map(o =>
      index.get(o.userId)
    )
    Build an id-to-user table once, then fetch by id directly. Like a phone book sorted by surname: you turn straight to the page.

    illustration: this order needs user-9

    map + find
    scanning from the start, 1 comparisons so far
    Map index
    index hits directly, 1 lookup

    2,000 orders x 2,000 userspress the button to start
    map + find
    Map index first
    dataset
    07React.memo

    Re-render only what actually changed

    A task list where each press updates the duration of exactly one row. The number on the right is how many times that row has rendered, counted for real in an effect.

    With both switches on, only the changed row flashes and its counter climbs — the other eleven stay at 1. Turn either switch off and every row re-renders, even though one field changed.

    Read the write-up
    click any row to re-run that task1 row re-rendered
    taskrenders
    React.memo
    useCallback
    08debounce / throttle

    Cut the number of events

    Every keystroke counts as one request that would be sent. Three strategies handle the same typing — see how many each actually sends.

    Type a burst quickly, then stop. Raw matches your keystrokes; debounce fires once after you pause; throttle fires at a steady rate while you type.

    Read the write-up
    raw0
    debounce0
    throttle0
    • · fires on every keystroke
    • · fires once 350ms after you stop
    • · at most once every 200ms
    09強制同步版面

    Read/write order decides the cost

    Both modes do the same work: change every box’s margin and read the layout. Only the order differs — read right after each write, or write everything then read once.

    Run transform first, then switch to left/top and watch the FPS drop. transform only touches compositing; left forces layout and paint for every element, every frame.

    Read the write-up
    ms per frame (layout)1400 boxes updated at once

    Switching to interleaved will visibly freeze the page — that is the point of this demo, not a bug. Stop still works, it just responds a beat late. Stop first if you want to switch modes smoothly.

    10AVIF / srcset

    How much one image can differ

    These are the actual assets used on /surf, not a made-up example. The same photo as AVIF and WebP at two widths, with bytes measured live.

    AVIF typically lands 30-40% under WebP, and a phone only needs the 768 variant — serving it 1920 wastes several times the bandwidth.

    Read the write-up
    a surfer breaking the surface, used to compare image formats

    Click a label to preview. In practice you never pick manually — correct sizes and srcset let the browser choose.