Back to Blog Β· Tue May 21 2024

Million Lint is now 1.0-rc

We are getting closer!

Two months ago, we announced Million Lint is in public beta. After 11,000+ beta testers (thank you!) and bugs, we are so excited to announce Million Lint 1.0.0-rc.

Million Lint is now generally stable, with no anticipated breaking changes between now and stable release.

If you've been waiting to explore since the public beta, you should definitely try it out. You can get started on any React project in one command:

Terminal
npx million@latest

Have a problem installing? Learn more by reading the installation guide.

Profilers are pure overhead

We created Million Lint to solve unnecessary re-renders in React without flamegraphs (traditionally used by React/Chrome profiler). This came out of our frustration with how difficult it was to make performant React websites.

Think a grid of product tiles, search inputs, or components that depend on state managers. It's painful to dig through the thousands of randomly colored rectangles with minified function names.

Figuring out the problem often leads to failure. It's never ending – why is this re-rendering? What triggered this issue? Maybe I should ask ChatGPT. Nope, it didn't give me anything useful.

Suddenly, 8 hours has gone by and you're still stuck. Not only that, you're now behind on the 100 other tasks in your backlog. Slow sites are bad for business. But because you can't figure it out, you have no choice but to give up.

Introducing Million Lint: an IDE extension that automatically finds and fixes performance issues for you!

No more guessing or getting stuck. It takes seconds to find issues, and minutes to solve them.

Check it out! πŸ‘‡

React Profiler Flamegraph

Profilers are an implementation detail

Optimizing performance is hard because:

  1. πŸ‘Ž Profilers distract you from coding (it's your responsibility to record and analyze).
  2. πŸ‘Ž Flamegraphs are difficult to read and interpret.

By abstracting away the profiler, we solve both problems.

  1. βœ… We automatically display performance data in your IDE.
  2. βœ… We help find and fix the issues for you.

Want to automatically audit your application for performance issues? Check out these examples:

Automatically fixing an input lag problem. It's like ESLint + code fix, but for performance!

Limitations of current solutions

We've already covered the user experience limitations of today's profilers. They also come with a slew of technical limitations. They require inspecting source maps and computionally expensive traversal over React fibers – adding extra overhead and complexity that slows down your app during development.

In May 2024, React Compiler was open sourced. It's a drop in optimizing compiler that helps memoize React components.

Here are some of the common performance issues it can fix:

Problems React Compiler can fix

{ data, setData } is an object that is recreated every time the Parent re-renders. React Compiler fixes this by memoizing the object.

function Parent() {
  const [data, setData] = useState([]);

  return <ChildComponent state={{ data, setData }} />;
}

Note: "With React Compiler" isn't the actual compile output, rather a conceptual representation of what the compiler would do. Check out the React Playground to see the actual output.

If you have many of these performance issues in your codebase (and/or have existing useMemo/useCallback in place), React Compiler will optimize it for you.

React Compiler is static analysis and code transformation at its core. This limits optimization opportunties as it doesn't have access to the runtime. It will prioritize correctness over making a change that may not be correct from its perspective.

Million Lint has access to source and runtime data. This gives it much more creativity to optimize your code.

Here are some examples of common performance issues React Compiler can't fix yet πŸ‘Ž, but Million Lint can potentially fix now βœ…:

Problems React Compiler can't fix yet

setFilter() causes the items.filter(...).map(...) to re-compute every time List re-renders. Even if you memoize it with the dependency array [filter], it will bust the memo every time the user changes the input value. The solution is to split the input state into two useStates, one that updates based on user input and one that is concurrent and updates the items.

function List({ items }) {
  const [filter, setFilter] = useState("");

  return (
    <>
      <input value={filter} onChange={(e) => setFilter(e.target.value)} />
      <ul>
        {items
          .filter((item) => item.text.includes(filter))
          .map((item) => (
            <li key={item.id}>{item.text}</li>
          ))}
      </ul>
    </>
  );
}

Note: Million Lint can potentially suggest an incorrect solution. We've designed it to apply the correct optimizations, but it's not perfect. We're always open to feedback.

Profile-guided optimization

Million Lint achieves this through Profile-guided optimization (PGO).

At build time, React Compiler will compile your source code and generate an optimized output.

Million Lint doesn't optimize your code at build time. It measures the runtime performance to make optimizations.

An analogy: If React compiler is spellcheck, Million Lint would be Grammarly.

Million Lint vs React Compiler

Under the hood, we use a compiler, runtime instrumentation, and language model workflow to fix performance issues. Here's how it works:

Compiler injects instrumentation into your code.

function App({ start }) {
  Million.capture({ start }); // ✨
  const [count, setCount] = Million.capture(useState)(start); // ✨

  useEffect(
    () => {
      console.log("double: ", count * 2);
    },
    Million.capture([count]), // ✨
  );

  return Million.capture(
    // ✨
    <Button onClick={() => setCount(count + 1)}>{count}</Button>,
  );
}

Instrumentation calls are executed when the app runs. Data is collected in the background. This data will be stored and visually displayed in your IDE.

[
  'src/App.jsx': {
    components: {
      App: {
        renders: [{ count: 7, time: 0.1 }, ...],
        instances: 3,
        count: 70,
        time: 1,
        location: [13, 0, 23, 1],
      }
    }
  }
  // and so on...
];

Data is formatted into a series of prompts for a language model workflow to determine problems and solutions. Here are some prompt formats we use:

'''{language}
{source}
'''
Is component a long list that needs virtualization? YES/NO

React Compiler, etc., with enough effort, can one day become a sufficiently smart compiler. But that day is not today.

However, Million Lint's profile-guided optimization can solve the many performance problems that need to be optimized today.

Note: Million Lint can handle most of the performance optimizations React Compiler can do today (if you can't use React Compiler). However, we recommend using React Compiler in tandem with Million Lint, since it is much more fine-grained in its memoization and reduces useMemo/useCallback code noise.

Soon: Million Lint in production

In the past 2 months, we optimized Million Lint to be ready for production.

Most performance issues are caught in production. For example, a developer may load just a few rows of test data, but a user could potentially load thousands. Much of this data is lost and code is never fixed. We realized we had to deploy Million Lint in production for it to catch more issues.

But we couldn't yet. Million Lint at public beta (2 months ago) was a whopping >100kB gzip at runtime. For comparison, @sentry/browser is 85kB kb gzip. Not to mention it totaled nearly ~32% of JS execution time.

So, we rewrote Million Lint from the ground up.

This optimized our build from >100 kb gzip to <2.9 kb gzip, and from ~32% of JS execution to <0.5%.

And here's a conceptual overview of our new architecture for production:

Since February, we have also been working with Faire, a large B2B eCommerce company. Performance is a top priority for them. Faire is using Million Lint internally to optimize performance issues (Look out for a case study on this soon!)

If you're interested in a demo, let's call!

What's next

We encourage you to install Million Lint 1.0-rc in your React codebase. There will be no anticipated breaking changes up to the stable 1.0. Following the release candidate phase, we'll be continuing efforts to improve automatic issue detection and code fixing.

This is just the beginning! Moving forward, we are excited to tackle performance issues with animations, bundle sizes, waterfalls, etc. Our eventual goal is to create a toolchain which keeps your whole web infrastructure fast, automatically - frontend to backend.

Have feedback? Let us know! Twitter and Discord. And if you're interested in joining Million, solve our challenge and send me your approach [email protected] :)

Acknowledgements

Special thanks to Theo, Ben, Jack, Ken, Igor, Deet, Dhravya, Sarthak, Britton, Daniel, Zeu, and Sunil for the video feature! A huge thank you to the 11,000+ beta testers and our friends at Faire (s/o Jude) for investing their time in testing our public beta. Thank you to Ivan for advising us along the way and developing our benchmark. We are eternally grateful for your help.

Slow React websites will soon be a thing of the past. Install Million Lint 1.0 today!

πŸ’œ The Million team,

– Aiden, John, Nisarg, Xinyao, Alexis

The team

Posted by

Aiden Bai
Aiden Bai

@aidenybai

John Yang
John Yang

@fiveseveny

Nisarg Patel
Nisarg Patel

@nisargptel

Xinyao Chen
Xinyao Chen

@xinyao27