Rzemiosło · The Craft
What is The CraftLevelsGet startedChapters Search Download PLEN
Level 3 · Codex ~3 min read

In one sentence: Measure first, then optimize: indexes, no more N+1, streaming — speed proven by numbers.

This chapter in plain terms

This chapter has one motto: performance is measurement, not a hunch. “It feels faster” doesn’t exist — there are numbers before and after. First you measure what’s slow, you fix it, and you prove with a number that it helped. Otherwise “optimisation” is sometimes a regression in disguise.

On the browser side what matters is what the user perceives as “fast”: don’t load images that aren’t visible, serve photos in a light format (WebP), and measure the whole thing with Lighthouse (built into Chrome) and the Core Web Vitals metrics.

On the database side the most common killer is a missing Index (database) — A lookup in the database that makes searching instant instead of scanning everything in order. The first move for slow queries — like an index at the back of a book instead of reading 400 pages. on a frequently queried column — the database then scans the whole table instead of looking under the keyword. The second is N+1 — A performance bug: the app queries the database 200 times instead of once, asking separately for each item. The most common cause of a “laggy” list. Like 200 phone calls instead of one request for the whole list.: two hundred separate queries where one would do.

A fair amount of space goes to mobile, because it’s tricky: the address bar and keyboard change the screen height on the fly. Hence 100dvh instead of 100vh and measuring the real viewport — otherwise buttons hide under the keyboard.

Example of N+1: a list of 200 products where the code asks the database for the price separately for each is 200 calls instead of one request for the whole list. The result: a “laggy” catalogue that, after adding a single bulk query, springs to life.

Read more: database index · the N+1 problem · dvh and the mobile viewport.

13 — Performance: frontend and SQL

Commandment III in pure form: verify, don’t declare — applied to speed. “Feels faster” doesn’t exist. Before/after numbers exist.

Performance isn’t a hunch, it’s a measurement. The most common optimization mistake: guessing what’s slow and “fixing” it without proof it was the problem. Measure first, fix what the metric points to, and prove with a number that you improved it. Performance is also SEO (Core Web Vitals — Google’s measures: how fast a page appears, how stably it lays out and how fast it reacts. They affect your Google ranking and whether the user stays. Measurable, so you can improve them. → 10) and cost (a faster query = a cheaper server → 12).

Measure first

  • Lighthouse / Core Web Vitals: LCP (largest contentful paint), CLS (layout shift), INP (interaction), TBT. These are hard numbers, not impressions.
  • Before/after, not “feels faster.” E.g. home-page optimization: preload hero, GPU-promoted animations, content-visibility: auto below the fold; you report LCP before and after, not “probably lighter.” → 03
  • SQL — The query language for a database — the way you “ask” the database for data or change it. The universal standard for talking to a database. A badly written query can bog down the whole app.: EXPLAIN QUERY PLAN tells you whether a query uses an Index (database) — A lookup in the database that makes searching instant instead of scanning everything in order. The first move for slow queries — like an index at the back of a book instead of reading 400 pages. or scans the whole table.

Frontend

  • Code-splitting + lazy-load below-fold — don’t load what the user can’t see.
  • Images: WebP (e.g. convert everything to WebP q=95), responsive srcset, preload hero (LCP), the rest lazy.
  • Fonts: self-host (e.g. Playfair/Outfit, as on jakub.solutions), font-display: swap — text visible before the font arrives.
  • content-visibility: auto on sections below the fold — the browser skips rendering what’s offscreen.
  • GPU-promoted animations (translateZ(0)/transform) instead of layout-triggering properties.
  • Static Cache — A temporarily remembered result, so the same thing isn’t computed again on every request. Speeds the app up, but can be a trap: a stale cache shows old data.: immutable + long max-age in prod (e.g. 7 days immutable in prod).
  • Cache-busting per deploy: since CSS/JS are immutable, append ?v=<git-short-hash> to every link — each deploy changes the URL → fresh fetch. Without it the user sees the old app (broken layout) until a hard refresh (→ 05).
  • Streaming — Sending a large response in chunks, as it goes, instead of loading everything into memory at once. The app doesn’t choke on huge data — the user sees a result sooner. / SSE for chat — the LLM response token-by-token (e.g. via SSE), the user sees the first words right away instead of a blank screen until generation finishes. → 08
  • Server-side pagination — never ship the whole catalog to the browser (e.g. server-side pagination for a few thousand items).

Mobile: the viewport is a moving target

On a phone the viewport is not a fixed rectangle: the URL bar grows and shrinks as you scroll, and the on-screen keyboard eats the bottom half. Treat it as static and you get elements hidden behind the keyboard, or a tall empty band where the URL bar used to be. Measure the real viewport — don’t assume it.

  • Use the dynamic unit. 100dvh (dynamic viewport height) for a full-screen sheet — not 100vh (which is the largest viewport and runs under the URL bar). Declare height: 100vh first as the fallback, then height: 100dvh to override — property order matters, an old engine keeps the last rule it understands.
  • Pin keyboard-sticky elements to visualViewport, not the layout viewport. A chat input or a sticky CTA on position: fixed; bottom: 0 is measured against the layout viewport and slides under the keyboard. The only reliable anchor is the visualViewport API — An agreed way for two programs to talk to each other — one asks, the other answers in a set format. Through an API your app connects to outside services (payments, maps, AI). Treat an API key like a password. (.height/.offsetTop, re-read on its resize/scroll) — it papers over the different iOS vs Android keyboard behaviour. Snap to the new size; don’t animate the keyboard open (a transition/transform on the keyboard event produces a visible jump).
  • A flex-basis set for a row becomes a HEIGHT in a column. flex: 1 1 220px is a width hint in a row; the same rule under a flex-direction: column breakpoint makes a 220px-tall empty box. Reset the basis (flex: 0 0 auto) at the breakpoint where the main axis flips.
  • The emulator lies; the DOM and the device don’t. A desktop browser’s device-mode renders dvh/fixed elements differently from a real phone — don’t “fix” a layout off a screenshot. Measure getBoundingClientRect()/getComputedStyle() on the real elements, and keep a device-profile test project (e.g. a separate Playwright project pinned to a phone) that asserts geometry every run: no horizontal overflow, tabs on one line, the key element above the fold. Verify, don’t declare (→ 03).

SQL

  • Indexes on columns in WHERE and JOIN — a hot query without an index is a full table scan.
  • Partial indexes — e.g. uniq_prices_active … WHERE expired_at IS NULL (an index only on active prices — smaller, faster, enforces uniqueness, → 11).
  • EXPLAIN QUERY PLAN before and after adding an index — proof the plan changed.
  • Avoid N+1 — A performance bug: the app queries the database 200 times instead of once, asking separately for each item. The most common cause of a “laggy” list. Like 200 phone calls instead of one request for the whole list. — no query in a loop per row; batch/join in one shot.
  • SELECT only the columns you need — not SELECT * when you need three fields.
  • Server-side pagination + cached aggregates (e.g. site_stats instead of COUNT(*) over the whole database on every visit to the home page).
  • WAL — Write-Ahead Log: a database mode where changes go to a journal first, then to the data. Gives safety (recovery after a crash) and better read/write concurrency. (SQLite — The simplest database — the whole thing lives in one file, with no separate server. A great default to start: zero configuration, easy backup (you copy the file). As you grow, you migrate up.) — readers don’t block the writer; the default mode in the reference project.
  • Careful with LIKE '%foo' — a leading wildcard kills the index (full scan); consider FTS if it’s a hot search path.

Anti-patterns

  • 🚫 Optimization without measurement — “I improved it” without knowing whether it was slow (→ 03).
  • 🚫 No index on a hot query — the most common cause of a slow catalog page.
  • 🚫 N+1 in a loop — 200 queries where one join would do.
  • 🚫 SELECT * — you transfer and deserialize columns you don’t use.
  • 🚫 Client-side pagination of huge sets — shipping a few thousand records to show 20.
  • 🚫 Blocking the UI waiting for the full LLM response instead of streaming (→ 12).
  • 🚫 No cache on expensive aggregates — COUNT(*) over the whole database on every request.
  • 🚫 100vh for a full-screen mobile sheet — the bottom hides under the URL bar/keyboard (use dvh + visualViewport).
  • 🚫 position: fixed; bottom: 0 for a keyboard-sticky input — it slides under the keyboard (anchor to visualViewport).
  • 🚫 A flex-basis carried from a row into a column breakpoint — a tall empty box (reset the basis where the axis flips).
  • 🚫 “Fixing” mobile from a desktop-emulator screenshot — you fix an artifact; measure real DOM geometry / test on a device profile.

For new projects

Add to Day 0 (→ 07): a Lighthouse baseline right after the first working page (you have a reference point), indexes on filter columns from the first Migration (database) — A controlled change to the database layout — adding a column, a table or moving data — step by step. Like a renovation to plan: rebuilding data in a set order so nothing “collapses”., EXPLAIN QUERY PLAN as a habit on every hot query. The overriding rule: no optimization without a before-and-after number — because without proof an “optimization” can be a regression in disguise (→ 03). Speed is both SEO (→ 10) and infrastructure cost (→ 12) — one investment, three returns.

The canonical doctrine is written in English.