首页 / Skills / 开发提效 / 性能优化 Performance Optimization
开发提效

性能优化 Performance Optimization

Addy OsmaniMITSkill 语言: 英文
协议已核对不含脚本低风险
演示真实运行记录 · claude-sonnet-5-5 · 2026年10月
Our product listing page is slow (fictional example): Lighthouse on mobile shows LCP 4.8 s, and the /api/products endpoint has p95 1.2 s. The endpoint code is below. What would you measure first, what is the likely bottleneck, and what would you change? We have a Postgres database and a React front end. ```js app.get('/api/products', async (req, res) => { const products = await db.query('SELECT * FROM products'); for (const p of products.rows) { p.category = (await db.query('SELECT name FROM categories WHERE id = ' + p.category_id)).rows[0]; p.reviews = (await db.query('SELECT rating FROM reviews WHERE product_id = ' + p.id)).rows; } res.json(products.rows); }); ``` The page renders about 1,200 products at once, each with a full-size 2400px JPEG hero image.
Skill: performance-optimization

There are two separate problems here, one on the server and one in the browser. Both show up in the code and the page description, but neither is measured yet. I'd measure first and confirm which one dominates LCP.

What to measure first

  1. Network waterfall in DevTools, throttled to mobile. Look at three numbers:

    • TTFB and download time for /api/products.
    • The JSON payload size.
    • The total image bytes, and when the LCP element's request starts and finishes.

    This tells you whether LCP is gated by the API, the images, or both. The API call has to finish before React can render any <img>, so the delays add up in sequence.

  2. The LCP element in the Lighthouse or Performance trace. Check which image it is, and look at the breakdown into TTFB, resource load delay, resource load duration and render delay.

  3. Postgres query count and timing for one request. Turn on log_min_duration_statement = 0 temporarily, or use pg_stat_statements. Also run EXPLAIN ANALYZE on the individual queries. With 1,200 products, I expect about 2,401 queries per request.

Likely bottlenecks

Backend: N+1 queries. The endpoint runs 1 query for products, then 1 for the category and 1 for the reviews of each product, one after another with await. Even at about 0.4 ms each, that is around a second of sequential round trips. This matches the p95 of 1.2 s. The endpoint also has these problems:

  • SELECT * with no pagination returns all 1,200 products in one response.
  • It sends every individual review rating, when the list page probably only needs an average and a count.
  • The queries are built by string concatenation. The values are integer IDs from your own database, so this is probably not exploitable today, but it is a bad habit and should use parameters.
  • Check that reviews.product_id has an index. It may already, and the plan will tell you.

Frontend: images and DOM size. 1,200 full 2400px JPEGs is probably several to tens of MB on a phone. That is most likely the main reason LCP is 4.8 s. Rendering 1,200 cards also costs main-thread time, and it will hurt INP.

What I'd change

Change one thing at a time and re-measure after each, as in the sequence below.

API (do first, it's cheap and the cause is clear):

app.get('/api/products', async (req, res) => {
  const limit = Math.min(parseInt(req.query.limit) || 24, 100);
  const offset = Math.max(parseInt(req.query.offset) || 0, 0);
  const { rows } = await db.query(`
    SELECT p.id, p.name, p.price, p.image_url,
           c.name AS category,
           COALESCE(r.avg_rating, 0) AS avg_rating,
           COALESCE(r.review_count, 0) AS review_count
    FROM products p
    LEFT JOIN categories c ON c.id = p.category_id
    LEFT JOIN LATERAL (
      SELECT AVG(rating)::float AS avg_rating, COUNT(*)::int AS review_count
      FROM reviews WHERE product_id = p.id
    ) r ON true
    ORDER BY p.id
    LIMIT $1 OFFSET $2`, [limit, offset]);
  res.json(rows);
});

This turns about 2,400 queries into 1, selects only the columns the list needs, adds pagination with a stable order, and parameterizes the inputs. Make sure reviews(product_id) is indexed. If the aggregate is still slow, store avg_rating and review_count on products or in a materialized view. Keyset pagination is better than OFFSET for deep pages. Add HTTP caching headers or a short TTL cache only if the data is read far more often than it changes, and key the cache on the query parameters.

Front end (probably the biggest LCP win):

  • Show about 24 products per page with "load more" or infinite scroll, or virtualize the list. This is the largest single reduction in DOM size, bytes and requests.
  • Serve resized images: generate 320, 640 and 960px variants in AVIF or WebP at build time or through an image CDN. Use srcset and sizes so a phone downloads a 15–60 KB image instead of a multi-hundred-KB one.
  • Give every <img> a width and height to avoid layout shift. Use loading="lazy" and decoding="async" on everything below the fold. Give the actual LCP image fetchpriority="high" and don't lazy-load it.
  • Consider server-rendering or prefetching the first page, so the LCP image doesn't have to wait for the JS bundle and then the API call.

Verifying

Re-measure the same way as the baseline (same throttling, same data, repeated runs). Compare against run-to-run noise, and revert anything that doesn't clearly move the numbers. Then add a Lighthouse CI budget and field monitoring of LCP p75 with web-vitals.

My expectation, which the measurements need to confirm, is that the query fix takes the p95 from 1.2 s to a few tens of milliseconds. The image and pagination work should bring LCP into the "good" range (≤ 2.5 s). If the waterfall shows something different, such as a slow TTFB from somewhere other than the queries, follow that instead.

Test run on a fictional slow product page. The model said what to measure first, found the N+1 queries and the oversized images, and gave a rewritten endpoint. Its expected speed-up is a prediction, not a measurement, and the code was not run.

它做什么

坚持先测量再优化。它先定下 Core Web Vitals 的目标(LCP、INP、CLS),然后走一个循环:测量、找出真正的瓶颈、只修这一件事、验证并加以守护。它为 LCP 慢(服务端、资源加载、渲染延迟)、INP 差和布局偏移提供了决策树,并涵盖图片(尺寸、现代格式、优先级与懒加载)、包体积与拆分、渲染与重渲染控制、长列表、缓存,以及后端:N+1 查询、缺失的索引、无上限的列表和分页。它还列出常见反模式(没有基线就优化、没修好查询就先加缓存),并要求一次只改一件事、在 CI 里保留性能预算。

输出

先测什么、按顺序排列的可能瓶颈、带代码的修复方案,以及如何验证。

适合什么场景

页面慢、接口慢、Lighthouse 分数不达标,或在上线前规划性能工作。包内带有 Skill 引用的性能检查清单。

说明与风险

低风险:纯指令文件,没有脚本。它建议的是代码改动和测量工具(Lighthouse、DevTools、查询日志),由你自己运行。已用一个虚构的慢商品页试用一次:它先说明了要测什么,找出了 N+1 查询和过大的图片,并给出了重写后的接口。它预估的提速是预测而不是测量,代码也没有运行。共享的检查清单已一起打包并修正了路径。作者 Addy Osmani,MIT 协议。

SKILL.md 原文包内共 4 个文件