All Articles

This Pattern Will Wreck Your React App's TS Performance

Matt Pocock
Matt PocockMatt is a well-regarded TypeScript expert known for his ability to demystify complex TypeScript concepts.

A couple of years ago, Sentry were having big problems with their React app. They'd pretty recently migrated it to TypeScript from JavaScript. And the app was part of a large monorepo.

But the IDE performance was slow. You'd often need to wait a couple of seconds after making a change for the TypeScript language server to update. And running tsc would take a long time.

Now, this isn't unusual for a large TypeScript codebase. But the Sentry team had a hunch that something was wrong. The problem felt out of proportion to the size of the codebase.

It turned out that the issue, outlined by Jonas, was down to a single pattern.

How To Tank Your React App's TS Performance

In tons of places in Sentry's codebase, they were extending HTML types in React. For instance, defining ButtonProps would look like this:

tsx
import React from "react";
 
type ButtonProps =
React.HTMLAttributes<HTMLButtonElement> & {
extraProp: string;
};
 
const Button = ({ extraProp, ...props }: ButtonProps) => {
console.log(extraProp);
return <button {...props} />;
};

This means that you could pass in all the props that a <button> element could take, plus an extraProp:

tsx
<Button
extraProp="whatever"
onClick={(e) => {
(parameter) e: React.MouseEvent<HTMLButtonElement, MouseEvent>
}}
/>;

But it turns out that this pattern is devilishly slow. So Jonas, following the advice of the TypeScript Performance Wiki, changed each of these to use an interface instead:

tsx
import React from "react";
 
interface ButtonProps
extends React.HTMLAttributes<HTMLButtonElement> {
extraProp: string;
}

Suddenly, things got a lot snappier. The TypeScript language server was faster, and tsc ran quicker. Just from a little syntax change. Why?

Why Did This Happen?

You may have heard that interface is slightly faster than type. This is not quite true. In fact, interface extends is slightly faster than &.

In an earlier version of this article, I'd posted an explanation based on some fuzzy thinking which, thanks to my old colleague Mateusz Burzyński, I now understand was wrong.

The problem is more complex than I realised - check out this thread for his critiques and our investigations.

Hopefully, I can update this article again with a definitive description of why this happens - but nothing is simple when it comes to TypeScript performance.

Suffice to say - interface extends is generally faster than &, and so it proved in this case too.

Matt's signature

Share this article with your friends

`any` Considered Harmful, Except For These Cases

Discover when it's appropriate to use TypeScript's any type despite its risks. Learn about legitimate cases where any is necessary.

Matt Pocock
Matt Pocock

No, TypeScript Types Don't Exist At Runtime

Learn why TypeScript's types don't exist at runtime. Discover how TypeScript compiles down to JavaScript and how it differs from other strongly-typed languages.

Matt Pocock
Matt Pocock

Deriving vs Decoupling: When NOT To Be A TypeScript Wizard

In this book teaser, we discuss deriving vs decoupling your types: when building relationships between your types or segregating them makes sense.

Matt Pocock
Matt Pocock

NoInfer: TypeScript 5.4's New Utility Type

Learn how TypeScript's new utility type, NoInfer, can improve inference behavior by controlling where types are inferred in generic functions.

Matt Pocock
Matt Pocock