Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    A better React 18 startTransition demo

    Demoing startTransition is hard. Modern computers are too fast πŸ˜…

    The demo I sent you last week doesn't work. It's an aberration. A fluke of dev mode.

    Further testing showed that every render takes less than 1ms. startTransition and time slicing didn't even have time to kick in.

    πŸ’©

    So I built a better demo on stream, but a subtle gotcha got in the way. Dan from the React team helped me figure it out and we even found a bug in the alpha. Great success all around!

    You can try the final React 18 startTransition demo here

    React 18 startTransition demo with lag indicator

    The demo shows you what happens when every state change updates 1,000,000+ nodes. Slider on the left grows the tree, makes the problem worse – exponentially. Slider on top leans the tree, updates every node. Use it to see what happens :)

    Toggle the Use startTransition checkbox to compare behavior with and without the new feature. You should see your inputs laaaaaag without startTransition. When it's enabled, the urgent input update happens fast and the slow fractal updates later.

    If you don't see slowness, enable the artificial 0.1ms delay. That'll do it.

    The original fractal tree stress test is code from 2016 and I'm happy to report upgrading to React 18 worked without changes.

    You can see the full code on GitHub

    Why it's slow

    The pythagoras tree is a fractal. A deeply nested data structure that brings any rendering framework to its knees. Drawing to the DOM is the least of your worries.

    The left slider sets nesting level. At 20 (max) your computer will slow down for sure. My 2020 top-spec MacBook sure did πŸ˜…

    Growing a fractal tree

    Each layer spawns 2 branches. Every update touches every branch. That's 2^20-1 = 1,048,575 updates for every change in lean angle.

    That's what makes this a great stress test :)

    • user code: calculates all that data
    • framework code: propagates the updates
    • browser code: redraws the DOM nodes

    Yes WebGL or Canvas would make the draw step faster, but this is not a graphics demo. We're showing something important that React 18 unlocks.

    Think of each square as a separate React component in your app. A user action might update many of them. Each component on its own is fast to render, but the work adds up. And there's no one place to optimize 😱

    startTransition enables React to break down this rendering work into small chunks.

    This keeps the slider UI responsive even while your components are executing. Note that this is true even when you toggle the artificial delay (0.1 ms per square)! Even the fastest library that does updates synchronously will freeze when the slowdown happens in your code.

    React can pull it off because it breaks up the work instead of doing it all at once. 🀘

    Modern computers are fast, though, so you can get away with a lot before you'll need to think about startTransition. Here's a comparison from last year:

    2016 vs. 2020

    How we debugged the demo

    You can look at Chrome's performance tab to see what's up. You're looking for large chunks of synchronous work in event handlers – that's an opportunity for startTransition.

    Working code, startTransition disabled
    Working code, startTransition disabled

    When you enable startTransition, those big chunks of work split into quick urgent updates followed by the computationally heavy work.

    startTransition enabled
    startTransition enabled

    Quick way to test you're leveraging startTransition ✌️

    When I first showed my demo to Dan we found a fun alpha bug. An old Webpack polyfill from react-scripts 0.7 triggered a bug in React 18 alpha. Updates getting batched weirdly. They'll fix this before going stable.

    Profiler view when startTransition didn't work
    Profiler view when startTransition didn't work

    Always remember: realistic timing happens in production.

    A startTransition gotcha

    startTransition lets you mark calculations and updates as not urgent. React performs them later.

    You can use this for expensive data transformation, large computations, or complex rendering. Anything goes.

    startTransition(() => {
      // do slow work
      // this code executes synchronously
      // but state updates are marked as less urgent
    })
    

    Sounds easy when you read about it, then you get it wrong on stream πŸ˜…

    Use separate state for non-urgent updates

    My first attempt looked kinda like this:

    const [treeLean, setTreeLean] = useState(0)
    
    function changeTreeLean(event) {
        const value = Number(event.target.value);
    
        // update visuals
        if (enableStartTransition) {
            React.startTransition(() => {
                setTreeLean(value);
            });
        } else {
            setTreeLean(value);
        }
    }
    
    // ...
    <input type="range" value={treeLean} onChange={changeTreeLean} />
    
    <Pythagoras lean={treeLean} ... />
    

    Change input, call changeTreeLean as the event handler. Tell React to update state inside a transition.

    And everything's laggy πŸ€”

    Because both components depend on the same state ...

    You have to split that state:

    const [treeLean, setTreeLean] = useState(0)
    const [treeLeanInput, setTreeLeanInput] = useState(0)
    
    function changeTreeLean(event) {
        const value = Number(event.target.value);
        setTreeLeanInput(value)
    
        // update visuals
        if (enableStartTransition) {
            React.startTransition(() => {
                setTreeLean(value);
            });
        } else {
            setTreeLean(value);
        }
    }
    
    // ...
    <input type="range" value={treeLeanInput} onChange={changeTreeLean} />
    
    <Pythagoras lean={treeLean} ... />
    

    Looks weird to split state like that, but it makes sense. You're saying it's okay for these 2 components to be visually out of sync.

    Now the input field and the tree update separately. startTransition makes a big difference.

    React 18 startTransition demo

    Use React.memo

    const Pythagoras = React.memo(() => {
      // same component code as usual
    })
    

    Wrap expensive components in React.memo to give startTransition a chance to kick in. React needs to know it's safe to split the render step.

    Show transitioning state

    I thought this was neat. You can show the user that a redraw is happening.

    const [isLeaning, startLeaning] = useTransition()
    
    // ...
    
    function changeTreeLean(event) {
        const value = Number(event.target.value);
        setTreeLeanInput(value); // update input
    
        // update visuals
        if (enableStartTransition) {
            startLeaning(() => {
                setTreeLean(value);
            });
        } else {
            setTreeLean(value);
        }
    }
    
    // ...
    
    <svg style={ opacity: isLeaning ? 0.7 : 1 }
    

    The useTransition hook lets you access transition state. "Are we done yet?"

    Instead of calling startTransition, you call the function returned from the hook – startLeaning. Then you can access done-ness with the boolean – isLeaning.

    You'll notice the tree fades out on big re-renders.

    My biggest surprise?

    Take React code from 2016, update to React 18 alpha, touch nothing else ... and it works. No bugs 😱

    Had to update react-scripts from 0.7 to 4.0 though. An old Webpack polyfill was killing startTransition.

    That's why we torture alpha versions my friend. For bugs like that ✌️

    Cheers,
    ~Swizec

    PS: React For Dataviz is my course on pushing React to the max and fun stuff like this​. I think it's getting a new module or two when React 18 comes out :)

    Published on June 16th, 2021 in Technical, React, Frontend

    Did you enjoy this article?

    Continue reading about A better React 18 startTransition demo

    Semantically similar articles hand-picked by GPT-4

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn more

    Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.

    Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.

    Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help πŸ‘‰ swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.

    Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers πŸ‘‰ ServerlessHandbook.dev

    Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization

    Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections

    Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog

    Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❀️

    Created by Swizec with ❀️