5 things to make your app faster
You type and the letter shows up late, you open a menu and it just sits there. It is almost always one of these five things, and none of them makes you rebuild your app.
The prompt that checks your app against these 5 things and fixes what it finds.
Your app is slow. You type and the letter shows up late, you open a menu and it just sits there, or the page takes five seconds to appear.
It is almost never the server, and you do not need to switch languages. It is these five things, ALWAYS the same ones.
I explain them as if you had never written a line of code. And if you do not want to touch anything, the prompt is up there: paste it into your agent and let it do the work.
First of all, measure. If you do not write down where you are now, you will not know later whether you improved.
Go to PageSpeed Insights, paste your URL and write down these three numbers:
- LCP: how long the important part of the page takes to show up. Under 2.5 seconds is good.
- INP: how long it takes to react when you tap a button. Under 200 milliseconds is good.
- CLS: how much the page jumps while it loads (that button that moves right when you are about to tap it). Under 0.1 is good.
You do not need to learn them. Just write them down, because you will compare them at the end.
Index
1. Find and remove unnecessary renders
Your screen is made of pieces: the header, the search box, the list, every card. Each piece is called a component.
Rendering means a piece draws itself again. And here is the catch: when a piece redraws, every piece inside it redraws too, even if nothing changed for them.
Once costs nothing. The problem is when it happens in a chain.
Example: you store what the user types in the search box at the very top, on the page. Every letter redraws the whole page with its 500 cards. That is 500 cards per letter.
That is what you feel as a «heavy» app.
How to see if it happens to you: install the React DevTools extension in Chrome, open the Profiler tab and turn on «Highlight updates when components render». Now use your app: everything that redraws lights up. If you type one letter and half the screen lights up, there it is.
How to fix it:
- Each piece of data, in the piece that uses it. If only the search box needs its text, let the search box keep it, not the whole page.
- Do not put everything in the same box. React has «contexts», boxes of data that many pieces share. If the user, the theme and the cart live in the same box, every time the cart changes, everything that looks at the user redraws. One box per thing.
- In long lists, draw only what you see. If you have 1,000 rows and 15 fit on screen, draw 15. This is called virtualizing, and libraries like TanStack Virtual do it.
- Give every item in a list its ID card. In React it is called key: use the product id, never its position. If you use the position and delete the first one, React thinks they all changed and redraws them all.
- memo, useMemo and useCallback tell React «if this did not change, do not draw it again». Use them only where you saw the problem in the Profiler. If your project uses React Compiler, it does this for you.
2. Shrink the bundle with lazy loading
Your app is hundreds of files: yours and those of every library you installed.
So the browser does not have to ask for them one by one, they all get packed into one or a few files when you publish. That package is called the bundle.
Think of a suitcase. Everything you put inside, the user downloads BEFORE they can use your app.
And it is not just downloading it. The phone has to read all that code before the buttons respond. A 2 MB bundle on a normal phone is several seconds with the page in front of you but dead: you tap and nothing happens.
The funny part is that your code is usually the lightest thing in there. What weighs is the charts library that only shows up in the admin panel, the text editor that only appears when you write a comment, or the whole icon pack to use four.
How to see if it happens to you: there are tools that draw the inside of the suitcase, with every library at its real size. In Next.js it is @next/bundle-analyzer, and in Vite, rollup-plugin-visualizer. The three biggest ones will almost always surprise you.
How to fix it: with lazy loading, which means «load it when you need it». Whatever is not used on entry does not go in the suitcase. The editor downloads when someone taps «Comment», and the chart when you open its tab.
In code it is one line:
// Before: the editor is in the suitcase even if nobody opens it
import Editor from "./Editor";
// After: it downloads the first time someone opens it
import dynamic from "next/dynamic";
const Editor = dynamic(() => import("./Editor"));
// Outside Next.js, same thing with React
const Editor = lazy(() => import("./Editor"));And three more things that empty the suitcase:
- Take the piece, not the whole box. import debounce from "lodash/debounce" instead of import _ from "lodash". If you import the whole library, you carry the whole library.
- Use what the browser already has. To format dates and prices you have Intl, built in. Moment weighs almost 300 kB to do the same.
- Throw out what you no longer use. That library you installed for a test three months ago is still in the suitcase.
3. Optimize requests and use caching
Every time your app asks the server for something (the user, their orders, the products), that is a request.
Every request is a round trip. Even if the server answers instantly, the trip costs: from a phone, between 100 and 300 milliseconds each.
And there are three mistakes I find in almost every app:
- One after another. You ask for the user and wait. Then their orders and wait. Then the products and wait. Three waits, when you could ask for all three at once.
- Repeated. The header asks for the user, the menu asks for the user and the profile asks for the user. Three trips for the same data.
- Huge. You bring all 5,000 products with all their information to show 20 names.
How to see if it happens to you: press F12 in Chrome, Network tab, and reload the page. If you see bars that start right when the previous one ends, they go one after another. If you see the same address several times, they are repeated. And the Size column tells you how much each one weighs.
How to fix it: whatever does not depend on each other, at the same time. Repeated ones, with a library like TanStack Query or SWR, which makes one single trip when three pieces ask for the same thing and shares it. And huge ones, in pages and asking only for what you show.
// One after another: 900 ms const user = await getUser(); const orders = await getOrders(); const products = await getProducts(); // All at once: 300 ms const [user, orders, products] = await Promise.all([ getUser(), getOrders(), getProducts(), ]);
And then there is the cache, which is keeping an answer so you do not have to ask for it again.
If your categories change once a month, there is no point fetching them every time someone comes in. You keep them and that is it.
They can be kept in three places: in the user's browser, in a CDN (servers spread around the world that serve your site from the one closest to the user) or on your own server.
There is only one rule, and it is IMPORTANT: every cache has to know when it expires. If you do not decide it, one day you change a price and half the world keeps seeing the old one.
I went deeper into it in my article on how to scale your app to a million users.
4. Find slow queries and N+1 problems
Requests go from the browser to your server. Queries go from your server to the database.
A query is a question to the database: «give me this month's orders». If a request is slow, very often it is because there is a slow query inside it.
Why is it slow? Almost always because an index is missing.
An index is exactly the same as a book's index. Without it, to find something you have to read the whole book, page by page. With a thousand rows you do not notice. With two hundred thousand, every question takes more than a second.
And then there is the N+1, the most common mistake and the hardest one to spot.
You ask for the last 20 orders: 1 question. Then, to show each order's customer name, you ask one question per order: 20 more. Total, 21 questions for a single screen. With 100 orders, 101.
It is like going to the supermarket 21 times for 21 things, instead of going once with the list.
It happens a lot with ORMs (the libraries that let you talk to the database without writing SQL), because you write order.customer.name and it does not look like a question. But it is.
// N+1: 1 question for the orders and 1 more for each order
const orders = await db.order.findMany({ take: 20 });
for (const order of orders) {
order.customer = await db.customer.findUnique({ where: { id: order.customerId } });
}
// 1 single question: the orders with their customer inside
const orders = await db.order.findMany({ take: 20, include: { customer: true } });How to see if it happens to you: turn on your ORM's query log and load a screen. If you see the same question many times with only the id changing, it is an N+1.
For the slow ones, if you use Supabase you have Query Performance in the dashboard. In any Postgres, put EXPLAIN ANALYZE in front of the query: if it answers «Seq Scan» on a big table, it is reading the whole table. An index is missing there.
How to fix it: the N+1, by asking for the orders and their customers in one single question (a JOIN in SQL, or your ORM's include). The slow ones, with an index on the columns you search or sort by.
But do not go overboard: every index makes saving data a bit slower. Only put them where you really search.
5. Optimize fonts, images and scripts
The four previous points are about code. This one is about everything else your site downloads, and on most sites it is what weighs the MOST.
Images. You upload the cover photo straight from your phone: 4,000 pixels wide and 4 MB. To show it at 1,200.
- Modern format. WebP or AVIF weigh between half and a tenth of a JPG or PNG, and they look the same.
- At the size it is shown. If it is shown at 1,200 pixels, it should weigh what 1,200 pixels weigh. In Next.js, its Image component does it for you.
- With width and height set. That way the browser saves its spot before it arrives and the page does not jump. That jump is the CLS from the beginning.
- The ones below, when needed. loading="lazy" on the images you do not see on entry. And the main one, the other way round: with priority, because it is the one that decides the LCP.
Fonts. Every weight of a typeface (regular, bold, italic…) is a separate file. If you load six and use two, you are downloading three times as much for nothing.
Load only the weights you use, in woff2 format (the lightest), from your own site and with font-display: swap. That last one shows the text in a system font while yours arrives, instead of leaving the page blank. In Next.js, next/font does all of it for you.
Third-party scripts. Google Analytics, the support chat, the Meta pixel, the TikTok one, the heatmap you added for a test…
Each one is another company's code that downloads and runs on YOUR site. And together they usually weigh more than your own app.
Remove the ones nobody looks at. And the rest, let them load when the page already works, not before. In Next.js, <Script strategy="lazyOnload">; outside Next, the defer attribute.
How to see if it happens to you: in the Network tab, sort by size. Whatever is on top is the first thing to fix. PageSpeed Insights also tells you by name («Properly size images», «Reduce unused JavaScript») and with the kB you would save.
Summary
- Measure first: LCP, INP and CLS in PageSpeed Insights.
- Renders: each piece of data, in the piece that uses it. In long lists, only what you see.
- Bundle: the suitcase the user downloads before they can use your app. Whatever is not seen on entry, out.
- Requests: at the same time and not one after another, one per piece of data, and with a cache that knows when it expires.
- Queries: an index on what you search and one single question instead of N+1.
- Images in AVIF at their size, only the fonts you use and third-party scripts last.
None of the five makes you rebuild your app. They are odd afternoons, and the first one you look at almost always shows.
My advice: start with the one PageSpeed flags. Fix it, measure again and move to the next one. ONE at a time.
And if you want it done
Copy the prompt above and paste it into your agent with the project open. It goes through the five in this order, measures before touching anything and gives you back a before-and-after table.
And when you have it faster, send me the numbers on Instagram, that kind of thing makes my day.