• Labs icon Lab
  • Core Tech
Labs

Guided: Optimization in React

In this guided lab, Optimization in React, you will learn to diagnose and mitigate wasted renders, implement memoization strategies using useCallback and useMemo, and optimize context and prop handling to improve your app's efficiency. You’ll also explore advanced concepts such as managing external API connections and leveraging React.memo for fine-tuned render control. Whether you’re aiming to boost your app’s responsiveness or deepen your understanding of React’s performance tools, this lab provides essential skills and insights for writing high-performance applications.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 1h 37m
Published
Clock icon Aug 01, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Overview

    Welcome!

    In this guided lab, you will optimize a simple helpdesk application to fix performance issues. As you perform these tasks, you'll learn how to diagnose issues, reduce wasted renders, and fix common React performance problems.

    What to Expect

    You should have foundational experience with React and have built a full application before.

    info> The React Foundations lab series will teach you the fundamentals and uses the same Helpdesk demo application.

    The JavaScript code in the lab is written in idiomatic React, which is typically ES2015+ syntax. You should be able to work with arrow functions, Promises, ES modules, and let/const.

    Additionally, you should be passingly familiar with the React Developer Tools and ideally have used the Profiler feature in both the browser's developer tools and React Developer Tools.

    info> Never used the React Dev Tools? Watch the 3-minute tour in the React Debugging Playbook course. To get more familiar with the browser and React profiling tools, watch the Conducting Performance Audits module in the React Performance Playbook course.

    info> Stuck on a task? Check out the solutions/ folder in the project directory for full solution files and reference code for each step. Files with numbers (like index.2.jsx) correspond to the task order in the step.

    Getting Started

    To make sure the lab environment is set up and working, you'll start by running the Globomantics Helpdesk demo application. Once set up, you can visit {{localhost:5173}} to view the app in your browser, or in the Web Browser tab in the editor to the right.

    info> Web browser not working? Sometimes while reading through the lab, the dev server will shut down and you'll have to reconnect to the Terminal. Just run npm run dev again to start the server back up. Other times, the web browser may not always reflect the latest code due to the auto-save feature of the lab. Just click the refresh button next to the address bar in the tab to force-refresh the page.

    How is the app hosted? You may wonder what "VITE" means and how the app is being hosted on the above URL. This comes from the JavaScript development tool Vite.js.

    Vite is a frontend tool that bundles and builds JavaScript applications, including React. There are other options available depending to build and host React applications depending on what you need, such as Next.js.

    Vite is versatile and supports many other frameworks and libraries, including plain HTML and CSS with very little configuration.

    A Simple Helpdesk App

    The application is a simple helpdesk where you can submit and view support tickets related to fictitious Globomantics products.

    A quick tour around the codebase:

    • src/components holds different components that are on the page
    • src/hooks holds a custom hook to work with IndexedDB, the web-based storage engine and the idb package
    • src/contexts hold a shared context to share state in the app
    • App.jsx is the main component that renders the page
    • The TicketForm component encapsulates the form fields and handling form submission
    • The TicketDisplay component handles displaying ticket information to the user
    • The TicketList component handles displaying the list of tickets in the database

    Additionally, the file tree has been organized into modules within the src folder so that it's easier to focus on the code you'll write throughout the lab.

    Scale exposes hidden problems

    Ticket data is stored in IndexedDB which is created when you run the app for the first time.

    If you run the app without any ticket data, you are unlikely to notice any performance issues. This is common in web development -- problems don't start appearing until you hit a certain level of scale with your application, and usually it's data or state management that exposes problems in a client-side React application. Once that happens, it becomes crucial to know how to diagnose and debug problems.

    You are not your end-user

    For this lab, the database is seeded with 10,000 tickets to exacerbate any performance issues. Depending on your local machine, the performance issues may be significant, or they may be minor. Remember though, you are not your end-user and they are likely to experience performance problems well before you.

    Poor performance impacts the business

    Performance issues can impact the user experience which can impact business metrics. The director who hired you on as a consultant told you so:

    Senior Director: These performance problems are impacting our "Time to Fix" support metrics. The support organization is losing approximately 90 hours a month to performance degradation, lowering our ticket response times, and we won't be able to budget for additional headcount unless we turn this metric around.

    Time's a wasting, get going!

  2. Challenge

    Optimizing Reference Handling

    Optimizing Reference Handling

    Where do you even start when diagnosing performance problems?

    Your eyes!

    When diagnosing performance issues, pretend you're Sherlock Holmes. Notice everything -- it could be important!

    Sometimes issues are observable to the naked eye without any special tools and those can be the easiest to tackle first.

    The case of the curious console

    Go ahead and open the browser's Developer Tools (F12) and see what the console is showing.

    Do you see something curious?

    There are two of the same exact messages:

    Successfully connected to IndexedDB

    Plus an error:

    DOMException: Key already exists in the object store.

    If you follow the message stack trace, you will find yourself in the src/hooks/useTicketDb.jsx file.

    Running effects on mount

    The console message originates in a Promise then method, after successfully connecting to IndexedDB.

    openDB(...).then(
      (result) => {
        console.info("Successfully...", result);
    
        return seedDb(result).then(() => setDb(result));
    });
    

    Using the powers of deduction, you believe this means that openDB is being called twice -- and you'd be right. But why?

    The useEffect has an empty dependency array ([]) which means it only runs once on mount. Shouldn't the effect only be run once during the lifetime of the app?

    That depends on where the hook is being used.

    Hook instances

    After searching around, you identify the useTicketDb hook is being used in two places:

    • src/App.jsx
    • src/components/TicketList.jsx

    Used in two places and two console log messages? A mere coincidence? Or a conspiracy?

    Turns out this is by design. A React hook is a function which means if you invoke it in multiple components, it will run its effects each time. Each time you use a hook, you get a new instance of it.

    Controlling object lifetime

    Normally this is acceptable and expected behavior but other times, like when connecting to a database, it's not desirable. You don't want to call openDB twice, you want to initialize a single reference to the database and maintain its lifetime over the app.

    To address this, you will need to lift state up -- but where should it go? The hook keeps a useState for the db instance. Can it go any higher? You could store the db reference in a Context which is only initialized once but in this case, it's probably easier to lift state into the module scope.

    Leveraging module state

    An ES module is only loaded once by default for the lifetime of an application. This means any variables in the top-level scope are maintained and only initialized once.

    The goal is to wait for the DB connection process to complete and only connecting once, while maintaining a single db instance.

    openDB returns a Promise, which can be stored in module state. This allows each hook instance to "wait for" the Promise to resolve. The openDB Promise will set a db variable in module state so only one instance is maintained.

    Since setting module state variables will not "notify" React that the DB connection is ready, you will also need an initialized state within the hook to trigger a re-render. If you reload the application and check the browser console, you should only see one message:

    Successfully connected to IndexedDB

    Conveniently, fixing the multiple db instances also fixed the red error message! Often in performance work, it's best to tackle one issue at a time as issues can be linked together. In this case, since the seedDb function was being called twice, there was a race condition in seeding the database rows with auto-incrementing IDs. This is also why you may have noticed there are 20,000 tickets, not 10,000 like originally intended.

    The usefulness of magnifying glasses

    What can't be seen by the naked eye requires special tools. In detective work, it might be the iconic magnifying glass, fingerprint testing, or surveillance footage.

    For React performance debugging, there are several useful tools:

    • A code linter
    • An interactive debugger
    • The React Developer Tools
    • Browser script profiling
    • Google Lighthouse
    • WebPageTest

    In this lab, you'll become familiar with the first four tools. The rest you can learn outside of this as part of general web performance work.

    Linting your code

    ESLint is a code-based tool that inspects your React app for common issues. These issues can lead to performance problems and should usually be addressed.

    Go ahead and run the lint command to see what unaddressed issues may be lurking in the codebase. Ah-hah! There are two issues that need to be fixed.

  3. Challenge

    Optimizing Hook Dependencies

    Optimizing Hook Dependencies

    ESLint uses different "rules" to lint your code and they are noted on the right-side of the messages.

    The two warnings appear to be coming from the react-hooks/exhaustive-deps rule:

    React Hook useEffect has a missing dependency

    React hooks should declare all their dependencies on external variables in their dependency array. It's easy to miss this, so the lint rule checks to see if you exhaustively list the hook's dependencies.

    Fixing exhaustive-deps violations

    The first warning suggests fixing the issue by adding getAllTickets to the hook dependency array in the TicketList component.

    Why don't you try it out? If you reload the page... uh-oh, something weird is going on. The ticket list keeps loading in an infinite loop!

    Debugging infinite hooks

    Since the only thing that changed is adding getAllTickets to the dependency array, it makes sense that this is what's causing an infinite loop -- but why?

    Remember that a hook will re-run when any of its dependencies change. But getAllTickets is a function -- how is it changing?

    To debug this and understand what's going on, you will need to use the browser's native JavaScript debugger to step through the code. Once you add the debugger; statements, the browser developer tools should pop open and pause on the line of code. It should look like this:

    Browser debugger paused in useTicketDb

    info> Debugger didn't pop up? You may need to open the developer tools manually by pressing F12 (for Chrome, Firefox, and Edge).

    Notice how the useTicketDb hook is returning an object with the getAllTickets property.

    Go ahead and hit "Resume script execution" (the play button) until you stop within the TicketList component. In the right sidebar you can inspect variables, and the getAllTickets is a function.

    If you click Resume again, and again, you'll see that you're always executing the useTicketDb hook and returning that object.

    When objects are not equal

    To the untrained eye, it may seem like getAllTickets is not changing -- but it is! Every time the useTicketDb function executes, it returns a new object instance every time. Since getAllTickets is an inline function, it's a new instance every time. Each instance has the same shape but it's not the same object instance. This means that the shallow equality comparison fails and the hook logic says:

    "Okay, getAllTickets has changed. I should re-run my effect!"

    And then you have an infinitely running hook.

    Reusing the same instance

    To fix this issue, somehow you need to have getAllTickets be the same instance across function executions. One way to accomplish this is by moving the functions outside the useTicketDb closure, effectively making them module-scoped functions and declared only once. If you reload the app, you'll see that everything is working again.

    info> Web Browser tab or page stuck? Infinite hooks can cause your browser tab or frame to hang. You may need to reset the lab environment by closing the Web Browser tab and re-opening it, or re-running npm run dev to restart the app, or reloading the whole page (don't worry, your progress is saved!).

    Now that the reference to getAllTickets is stable, the hook doesn't get stuck in a loop.

    This idea of "stability" is important and will help you fix the next lint warning...

  4. Challenge

    Memoizing Functions with useCallback

    Memoizing Functions with useCallback

    The next lint warning suggests the same kind of fix for the useEffect in the TicketPager component (in the TicketList.jsx module). This time after reloading the app, you may not notice anything immediately. However, try clicking "Next" in the paging component to view more tickets.

    What?! It resets back to showing the first page!

    Try adding a debugger; statement again to step through the code. Again, the browser developer tools should pop up with the debugger paused before the useEffect hook, like this:

    Browser debugger paused in TicketPager

    If you Resume execution, it hits the breakpoint again and if you keep hitting the play button, it will keep on going.

    It turns out, the component is re-rendering infinitely -- you just didn't see any difference because the DOM wasn't changing until you tried paging through the ticket list and it resets to page 1 every update.

    The importance of stability

    Previously, I said stability is an important concept. Since effect hooks re-run whenever their dependencies change, it means that you always want to ensure the values you pass in the array are stable -- meaning that if they don't materially change, their reference should not change.

    Could you fix this by moving the onPageChange to be a module-level function? Take a look:

    const TicketPager = ({ onPageChange, total }) => {
    

    No, the value is being passed as a prop to the component. This means the responsibility for passing a stable value moves up to the parent component.

    If you follow the call stack, you'll see:

    <TicketPager onPageChange={handlePageChange} total={allTickets.length} />
    

    In the TicketList component, and handlePageChange is defined as:

    const handlePageChange = (page) => {
    

    Can this function be moved to the module-scope? Again, no, because it references React component state (sortedAllTickets).

    How can you possibly fix this?

    1. The handlePageChange function is a new instance every time TicketList renders.
    2. It calls setVisibleTickets which causes TicketList to re-render (a React state change),
    3. Which then causes TicketPager to re-render (because handlePageChange is a new instance),
    4. Which then causes onPageChange(1) to be called (because the hook re-runs),
    5. Which then calls handlePageChange again...
    6. And on and on until you reach Infinity.

    There needs to be a way to stabilize handlePageChange so that it's not a new function instance every render.

    Memoization creates stable values

    Memoization is a pattern and approach that caches and reuses references based on declared dependencies. This means a value will remain referentially stable every time you ask for it unless a dependency changes. That sounds like hooks, doesn't it? They are built on the same concept.

    React provides built-in APIs for memoizing different things -- and one of those is callbacks like handlePageChange with the useCallback API.

    It only requires a small change to handlePageChange where you wrap the function with useCallback and then pass an array of dependencies:

    import { useCallback } from "react";
    
    const handlePageChange = useCallback((page) => {
    
    // ... logic
    
    }, [sortedAllTickets]);
    

    useCallback is a hook, so it works the same way as useEffect. If any dependencies change (via shallow equality), the hook returns a new instance of the callback function bound to the new dependency values. Otherwise, it reuses the previous reference and keeps the value stabilized.

    Why aren't set-state functions added as dependencies?
    React automatically creates a stable function for set-state actions, so they are not required to be passed to hook dependency arrays. However, ESLint may sometimes tell you to do so if you are passing a set-state action from a custom hook. It doesn't hurt to do it, but you can also explicitly [disable those lint errors](https://eslint.org/docs/latest/use/configure/rules#disabling-rules).
    
    After memoizing the callback, you can now page through the tickets as before.

    Why don't you run the npm run lint command again and see if there any more obvious issues to address? No warnings and no errors! Your work is done. Right?

    Observing running code

    Not quite. ESLint helps you fix common sources of errors with React apps, including performance issues like stabilizing hook dependencies. It is a "static analysis" tool, meaning that it only scans your source code -- it can't detect issues while the code is running (also called "at runtime").

    The industry calls measuring runtime code "observability" and there are some special tools for React that allow you to "see behind the curtain" and inspect how React is rendering your app in quite a lot of detail.

  5. Challenge

    Optimizing Context and Hook Re-Rendering

    Optimizing Context and Hook Re-Rendering

    To uncover issues that are harder to detect, you'll want to install the React Developer Tools. These will allow you to inspect and debug how a React app is rendering at runtime.

    Starting a profiling session

    Once you have the React Developer Tools installed, reload the demo app and there will be two additional tabs in the browser developer tool panel: Components and Profiler.

    Components and Profiler tabs in dev tools

    The Profiler allows you to measure and watch how React renders your application, either from the initial load or during an interaction sequence (like clicking around).

    Designing an audit scenario

    To conduct a performance audit, you have to design scenarios. Depending on what you want to test, you will need to design different scenarios that follow different paths through the app. This lab will walk through a couple key scenarios to show you how to diagnose different issues.

    In the first scenario, you are testing whether displaying a ticket might be causing any performance issues.

    You can follow these steps to perform your first profiling audit:

    1. Click the Profiler tab
    2. Click the Start Profiling button (🟠).
    3. Click the same ticket 3-4 times to display it

    The resulting profiler output will look something like this:

    React profiler output with annotations

    Interpreting the output

    Refer to the annotated numbers in the screenshot for reference

    React renders in "commits" which batch updates to the screen. This commit timeline (1) is in the upper-right corner of the profiler, and you can page through it to see the contents of each commit with timing data.

    The Flamechart (2) displays the tree of components and the sizes of the bars are the relative rendering time. You can select components to filter the display.

    The selected component or commit data is displayed in the details sidebar (3). This contains additional timing info, as well provides reasons why the component rendered (if that option is enabled in the Settings).

    Setting expectations

    Before performing an audit, it's important to note what you expect to see.

    Clicking on a ticket link once should select it and display it, which is what the profiler shows. However, clicking the same ticket multiple times should not result in extra commits -- because the ticket selection hasn't changed.

    Debugging hook changes

    According to the profiler session, the TicketContext is updating each time you click the ticket link and it says:

    Why did this render? Hook 2 changed

    The number refers to the "hook index" which is a 1-based number corresponding to the order of declaration in a component.

    In the TicketContext, there are two hooks declared:

    const [isCreating, setIsCreating] = useState(false);
    const [selectedTicket, setSelectedTicket] = useState(null);
    

    Therefore Hook 2 corresponds to selectedTicket state. The profiler is saying that selectedTicket is changing every time you click the ticket link.

    Passing unstable objects

    If you inspect TicketList component, you will find the following code that sets the selected ticket state:

    const handleTicketSelected = async (id) => {
      const ticket = await getTicket(id);
      setSelectedTicket(ticket);
    };
    

    The problem is that getTicket retrieves an object from IndexedDB which creates a new instance every time its called. ticket is an unstable object.

    Controlling Context re-rendering

    Since it's unstable, the TicketContext will re-render since the selectedTicket state is updated.

    When a Context updates, it causes all components that consume it to re-render. Since the TicketContext is used in the App component, it re-renders the entire application.

    It's important to control context updates as much as possible if you use them. In this case, it would be best to only call setSelectedTicket if the displayed ticket is different than the currently selected ticket.

    Conditional state updates

    There is an overload to set-state actions that accepts a callback that is passed the previous state value:

    setSelectedTicket(prevTicket => {
      // Return the same state as before 
      // which will NOT trigger a state update
      if (prevTicket?.id === id) {
        return prevTicket;
      }
    
      // Return new state that will trigger an update
      return ticket;
    });
    

    This is a great way to conditionally update state. Memoizing the handleTicketSelected callback and updating it to only call setSelectedTicket when the ticket ID changes successfully prevents wasted rendering when clicking the same ticket multiple times, as shown in this updated profiling session:

    After fixing selected callback

    In the session I clicked the same ticket several times but there are only two commits instead of four. The second commit is expected and only rendered when changing to a different ticket.

    Isolating Context rendering

    What else can you see in the profiling flamechart? Colored bars indicate the component updated, and the intensity of the color is the relative render timing.

    If you look at the second commit, you'll see that the App component and all components underneath it re-render due to "Context changed." Even though the commit time is below 2ms, it's not ideal that the entire App component tree is re-rendered when TicketContext changes. In a more complex app, this could be a significant performance problem.

    Sometimes to fix performance issues, you must refactor the application and move hooks closer to where they are needed to isolate their impact.

    Piecemeal refactoring

    Currently the App depends on the TicketContext but it doesn't need to -- the logic can be moved to other places in the component tree and safely removed from App to isolate the re-rendering.

    You'll refactor out each dependency on TicketContext one-by-one since this a major change.

    To begin with, you'll remove the dependency on selectedTicket by modifying how TicketDisplay works. Next, you can remove the hook dependencies on addTicket, setSelectedTicket, and setIsCreating by moving the handleTicketCreated callback logic into the TicketForm component. After refactoring App, running the same scenario steps again produces a much more greyed out flamegraph chart:

    Profiling session after refactoring App

    The grey components are not rendered during the commit, which is exactly what the refactoring was intended to do.

    List re-rendering can be expensive

    Do you notice how in the second commit, each of the TicketListItems are rendering? Why does every list item re-render when the displayed ticket changes? That doesn't seem right.

    Lists are a common source of performance issues. While displaying 20 tickets doesn't sound like much, each update takes between 0.2 to 1ms. That means re-rendering 1000 tickets in a single commit could lead to a 1 second slowdown depending on the client machine.

    Ideally, list items should only re-render when something they're displaying changes.

  6. Challenge

    Memoizing Components with memo

    Memoizing Components with memo

    If you select one of the TicketListItem components in the flamechart, it says it rendered due to the parent component re-rendering:

    Profiler showing how component re-renders due to parent

    This seems wasteful -- why should every TicketListItem re-render simply because its parent changed?

    How functional component rendering works

    This is by design. Functional components always render anytime their parent component is rendered, even if their props haven't changed.

    However, React does allow you to opt-out of this behavior using the memo API:

    import { memo } from "react";
    
    const MemoizedTicketListItem = memo(
      (props) => /* component code */);
    

    By wrapping a functional component in memo(), it will not re-render unless the props are different (and it even allows you to override the prop comparison function).

    This should be used sparingly, but in some scenarios like list rendering, it can provide some performance gains by avoiding re-rendering list items excessively. Memoizing the component and running the same scenario shows that TicketListItem no longer re-renders when changing tickets:

    Flamechart showing TicketListItem not re-rendering

    While the time savings in this demo lab is minor (~1ms), memoizing components can lead to significant performance gains for expensive list rendering.

  7. Challenge

    Memoizing Values with useMemo

    Memoizing Values with useMemo

    The React Developer Tools are an essential debugging tool to identify React rendering issues like excessive rendering. However, the browser's native performance tooling can also help identify potential CPU and memory issues.

    Sometimes CPU slowdowns can be impacting React rendering times but the React Developer Tools won't surface these problems.

    Running a CPU profile

    To run a CPU profile, in the browser developer tools, click the Performance tab (in Chrome or Edge).

    Click the Settings gear icon and select CPU throttling to add a 6X slowdown. Throttling your CPU can give you more realistic performance that matches an end-user device and can highlight issues you may be overlooking by using a powerful machine.

    Performance profiling settings

    You'll run the same scenario, clicking Start profiling, clicking on multiple tickets to display them, and then stopping the profiling session.

    You will see a very intimidating looking flamechart like this:

    Browser CPU profiler

    Reviewing source timings

    For the purposes of this lab, it will be easier to find issues by browsing the Sources tab. The Sources tab allows you to view JavaScript source maps for your application.

    After you run a browser profiling session, your JavaScript sources will have CPU timing data added to the margins next to your code. This makes it easier to identify parts of your code that could be slowing down the app.

    In the TicketList.jsx source map, notice how this line that sorts allTickets by ID takes up a relatively large amount of CPU cycles (~13ms vs. 0.5ms, or about 26X longer):

    Slow sort method

    Why don't you set the debugger to pause before that line of code and see what's potentially going on?

    info> If you were following along and turned on CPU throttling, you should now turn it off. Otherwise debugging this code will take much longer than usual. If you reload the app, the debugger should now pop up again (open the browser developer tools, if not).

    Animation of pausing/resuming

    If you keep pressing Play/Resume, what you should notice is that the breakpoint is getting hit very often -- much more than the 1-2 commits that the React Developer Tools showed.

    React updates vs. commits

    This is where the React Developer Tools aren't telling you the whole story. A React component may run its update logic multiple times before actually being scheduled for a render commit.

    This is so that any state updates can be handled asynchronously and batched together efficiently.

    However, it means that logic written in the component function body will execute every update which can be expensive.

    Memoizing values

    The sortedTickets value is based on the allTickets state. Whenever you need to create a derived value, the calculation is expensive, and you want it to be calculated only when that dependency changes, it is a good candidate for memoization.

    Unlike the memo API which is for components, and the useCallback hook which is for functions, React offers the useMemo hook for memoizing values:

    import { useMemo } from "react";
    
    const sortedAllTickets = useMemo(
      () => /* sort code */, 
      [allTickets]);
    ``` Enabling CPU throttling and running the CPU profile scenario again produces source timings that show much less total CPU time spent sorting tickets:
    
    ![Sorting source time](https://ps-cdn.s3.us-west-2.amazonaws.com/code-labs/public/909409b1-0078-4ac2-8d53-fa6ab7476ab8/0d39afc12493ab00b15c-1730219743.png)
    
    
    ## Reducing CPU usage helps reduce energy usage
    
    Incurring ~5ms once with a throttling enabled is orders of magnitude better than incurring 5ms every time you  change tickets. It may not seem like much, but in production applications there can be many innocuous lines like this in hundreds of components across the app -- it all adds up!
    
    Not only does reducing CPU cycle time help speed up the app, it also uses less energy and power on the user's machine. A win-win for all of us.
    
    
    ## Memoization is not a one-size-fits-all strategy
    
    So far you have fixed many of the performance issues through memoization. Does this mean you should always memoize _all_ components, props, and callbacks in React? **No.**
    
    The trade-off with memoization is increased memory usage. Since values are cached in-memory, the more objects you memoize (and the more they change), the more your heap size will grow. This is why the best performance fixes involve simplifying, refactoring, or reducing renders through other means besides memoization.
    
    Finally, the best performance advice is simply to _measure before changing anything._ Memoization, shared module state, and refactoring can sometimes increase the complexity of the code. Make sure the performance gain is actually worth the added complexity.
  8. Challenge

    Optimizing Slow Rendering

    Optimizing Slow Rendering

    Similar to the React Profiler, you will want to perform different interaction scenarios during an audit to uncover CPU slowdowns.

    For example, you could measure paging through tickets, a common interaction that support personnel will do.

    Taking another CPU profile with a 6X throttle and clicking "Next" a few times to page through the ticket list produces a CPU flamechart with a much more pronounced problem:

    Clicking taking too long

    Addressing perceived UX problems first

    Not only is the problem obvious in the CPU profile, when you clicked the Next or Previous buttons you probably felt the app was slow to respond to your click interaction. This is part of your perceived user experience. According to usability studies, users will perceive anything above 100ms to be delayed. The click events in the profiling output are each in excess of 400ms, or nearly half a second.

    When the issue presents itself on the flamegraph like this, you can hover over the chart and select bars to view the function call stack and timing data:

    Hovering over the function call

    In this case, the problem function is traced to handleNextPage in the TicketPager component. However, the actual code that is slow is in the callback handlePageChange function.

    You can see the code in the handlePageChange function has a very inefficient way of calculating visible tickets, iterating over the allTickets list and calling indexOf.

    Handling expensive calculations

    This is a case of an expensive calculation. Big-O notation describes the computational time complexity of an algorithm:

    Big-O time complexity chart

    Since the code iterates multiple times over the tickets array, the time complexity is quite bad, leading to a CPU slowdown.

    Rather than iterate over the array to calculate indices, this algorithm can be simplified to a single operation using the Array.slice method. Re-running the scenario with CPU profiling and throttling should now produce a much more compact flamegraph without the large chunks of processing time:

    Fixed paging slowdown

    In this profiler session screenshot, each click took about 40ms, a 10X improvement.

    Paging through the list should also feel faster and more responsive as it's below the 100ms UX mark.

    A snappy app

    It appears that your work is done! The support team is reporting a much faster perceived UX now that you've fixed a lot of the wasted rendering and CPU slowdown issues.

  9. Challenge

    Recap and What's Next

    Recap and What's Next

    The common thread for many of the performance fixes you implemented was that you were reducing wasted renders.

    Your goal is to reduce wasted renders

    The principal objective of optimizing React apps is to reduce wasted renders. Wasted renders use up CPU cycles, result in excessive DOM updates, and impact the user experience negatively.

    In this lab you reduced wasted renders by:

    • Using module-level state to control object lifetime
    • Refactoring and isolating hooks to only the components that needed them
    • Memoizing callbacks with useCallback to stabilize functions
    • Memoizing values with useMemo to reduce expensive computations
    • Memoizing list items to prevent them from excessively rendering due to their parent changing
    • Avoiding setting previous state that triggers unnecessary updates
    • Using the browser script profiler and the React Developer Tools to diagnose issues

    Learn more

    Optimizing React applications is a large topic and there are some advanced APIs that weren't covered.

    You can reference the following to learn more and dive deeper into optimization topics:

Kamran Ayub is a technologist specializing in full-stack web solutions. He's a huge fan of open source and of sharing what he knows.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.