From 2832306d95b7b7840ce6d54100f73f7da0869c81 Mon Sep 17 00:00:00 2001 From: Andrei Smirnov Date: Thu, 8 Oct 2026 07:41:01 +0300 Subject: [PATCH] Link web brief terms and remove bold Add external links to backend, frontend, nextjs, react and security, remove bold, update nextjs for Next.js 16.4, and fix facts flagged in review. Co-Authored-By: Claude Opus 5.5 --- docs/topics/backend.md | 124 +++++++++++------------ docs/topics/frontend.md | 114 ++++++++++----------- docs/topics/nextjs.md | 215 ++++++++++++++++++++-------------------- docs/topics/react.md | 214 +++++++++++++++++++-------------------- docs/topics/security.md | 180 ++++++++++++++++----------------- 5 files changed, 424 insertions(+), 423 deletions(-) diff --git a/docs/topics/backend.md b/docs/topics/backend.md index 4ccf095..8bef518 100644 --- a/docs/topics/backend.md +++ b/docs/topics/backend.md @@ -8,183 +8,183 @@ What experienced backend engineers forget before an interview, grouped by subtop | Codes | Meaning | |---|---| -| **401** vs **403** | missing or invalid credentials (re-auth may help) vs refused — credentials, if sent, are insufficient; others might succeed (or 404 to hide that the resource exists) | -| **409** vs **422** | conflicts with current state vs well-formed but fails validation | -| 202 / 204 | accepted for async processing / success with no body | -| 412 / 429 | precondition (`If-Match`) failed / rate limited | +| [401](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.2) vs [403](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.4) | missing or invalid credentials (re-auth may help) vs refused — credentials, if sent, are insufficient; others might succeed (or 404 to hide that the resource exists) | +| [409](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.10) vs [422](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.21) | conflicts with current state vs well-formed but fails validation | +| [202](https://www.rfc-editor.org/rfc/rfc9110#section-15.3.3) / [204](https://www.rfc-editor.org/rfc/rfc9110#section-15.3.5) | accepted for async processing / success with no body | +| [412](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.13) / [429](https://www.rfc-editor.org/rfc/rfc6585#section-4) | precondition (`If-Match`) failed / rate limited | ### Method semantics -| Method | Safe | Idempotent | +| Method | [Safe](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) | [Idempotent](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.2) | |---|---|---| | GET, HEAD | yes | yes | -| PUT, DELETE | no | **yes** | +| PUT, DELETE | no | yes | | POST | no | no | -| PATCH | no | not guaranteed — depends on the patch format | +| [PATCH](https://www.rfc-editor.org/rfc/rfc5789) | no | not guaranteed — depends on the patch format | ### PATCH formats -- **JSON Merge Patch** (RFC 7396): send a partial object; `null` deletes a field — so it can't set a value to null or edit one array element. -- **JSON Patch** (RFC 6902): a list of operations (`add`, `remove`, `replace`, `test`) on JSON Pointer paths; `test` makes the patch conditional. +- [JSON Merge Patch](https://www.rfc-editor.org/rfc/rfc7396) (RFC 7396): send a partial object; `null` deletes a field — so it can't set a value to null or edit one array element. +- [JSON Patch](https://www.rfc-editor.org/rfc/rfc6902) (RFC 6902): a list of operations (`add`, `remove`, `replace`, `test`) on [JSON Pointer](https://www.rfc-editor.org/rfc/rfc6901) paths; `test` makes the patch conditional. ### REST vs gRPC vs GraphQL -| | REST | gRPC | GraphQL | +| | [REST](https://en.wikipedia.org/wiki/REST) | [gRPC](https://grpc.io/docs/what-is-grpc/introduction/) | [GraphQL](https://graphql.org/learn/) | |---|---|---|---| -| Contract | OpenAPI (optional) | **Protobuf**, strict | schema, strict | -| Transport | any HTTP version | **HTTP/2** only | usually HTTP POST | +| Contract | [OpenAPI](https://spec.openapis.org/oas/latest.html) (optional) | [Protobuf](https://protobuf.dev/overview/), strict | schema, strict | +| Transport | any HTTP version | [HTTP/2](https://www.rfc-editor.org/rfc/rfc9113) only | usually HTTP POST | | Strength | HTTP caching, simplicity | streaming, speed, codegen | client picks fields, one round trip | -| Weakness | over/under-fetching | browsers need gRPC-Web | hard to cache, N+1, query-cost limits | +| Weakness | over/under-fetching | browsers need [gRPC-Web](https://grpc.io/docs/platforms/web/) | hard to cache, N+1, query-cost limits | ### Pagination -- **Keyset**: `WHERE (created_at, id) > (:c, :id) ORDER BY ... LIMIT n` — fast at any depth and unaffected by inserts; needs a unique, immutable sort key, or updated rows skip or repeat. -- Expose the position as an opaque **cursor**; a cursor is just a token and can also encode an offset. -- **Offset**: simple and jumps to page N, but slows with depth and skips or repeats rows as data changes. +- [Keyset](https://use-the-index-luke.com/no-offset): `WHERE (created_at, id) > (:c, :id) ORDER BY ... LIMIT n` — fast at any depth and unaffected by inserts; needs a unique, immutable sort key, or updated rows skip or repeat. +- Expose the position as an opaque cursor; a cursor is just a token and can also encode an offset. +- [Offset](https://www.postgresql.org/docs/current/queries-limit.html): simple and jumps to page N, but slows with depth and skips or repeats rows as data changes. ### Versioning -- URI (`/v1/`) is explicit and cacheable; a header (`Accept: ...;version=2`) keeps URLs clean but is harder to discover. -- Whatever the scheme: **additive changes only** within a version; removing or renaming a field is a breaking change. +- URI (`/v1/`) is explicit and cacheable; a header ([`Accept: ...;version=2`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept)) keeps URLs clean but is harder to discover. +- Whatever the scheme: additive changes only within a version; removing or renaming a field is a breaking change. ### Error responses -- One error shape everywhere — **RFC 9457 problem details** (`type`, `title`, `status`, `detail`). +- One error shape everywhere — [RFC 9457 problem details](https://www.rfc-editor.org/rfc/rfc9457) (`type`, `title`, `status`, `detail`). - Never return stack traces or SQL; log details server-side and return a request ID. ## Idempotency and concurrency ### Idempotency keys -- The client sends a unique key per logical operation; the server stores the key with the **first result** and returns it on every retry. -- Handle the **in-flight** case: a retry arriving while the first attempt is running must wait or get 409, not execute twice. +- The client sends a unique [`Idempotency-Key`](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/) per logical operation; the server stores the key with the first result and returns it on every retry. +- Handle the in-flight case: a retry arriving while the first attempt is running must wait or get 409, not execute twice. - Scope keys per client and expire them after a TTL (e.g. 24 h). ### Optimistic concurrency with ETags -- `GET` returns an **`ETag`**; the client sends `PUT` with **`If-Match: `**, and the server answers **412** if the resource changed — no lost updates between clients. -- `If-Match` needs a strong ETag (not `W/`); `If-None-Match: *` makes a `PUT` create-only. +- `GET` returns an [`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag); the client sends `PUT` with [`If-Match: `](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/If-Match), and the server answers 412 if the resource changed — no lost updates between clients. +- `If-Match` needs a [strong ETag](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.1) (not `W/`); [`If-None-Match: *`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/If-None-Match) makes a `PUT` create-only. ### Transactional outbox -- Write the business row and an outbox row in the **same DB transaction**; a relay (poller or CDC) publishes outbox rows and marks them sent. -- Fixes the dual-write problem without 2PC, which most brokers don't support. Delivery is at-least-once. +- Write the business row and an outbox row in the same DB transaction; a relay (poller or [CDC](https://en.wikipedia.org/wiki/Change_data_capture)) publishes outbox rows and marks them sent — the [transactional outbox](https://microservices.io/patterns/data/transactional-outbox.html). +- Fixes the dual-write problem without [2PC](https://en.wikipedia.org/wiki/Two-phase_commit_protocol), which most brokers don't support. Delivery is at-least-once (see [distributed.md](distributed.md)). ### Inbox deduplication -- Consumers record processed message IDs **in the same transaction** as their effect and skip repeats — the receiving side of the outbox. +- Consumers record processed message IDs in the same transaction as their effect and skip repeats — the receiving side of the outbox ([idempotent consumer](https://microservices.io/patterns/communication-style/idempotent-consumer.html)). - Expire entries only after the broker's maximum redelivery window. ## Service calls ### Timeouts and deadline propagation -- Every outbound call gets a **timeout**, shorter than the caller's own. -- **Propagate the deadline** (gRPC does it natively) so downstream services stop work once the original caller has given up. +- Every outbound call gets a timeout, shorter than the caller's own. +- Propagate the deadline so downstream services stop work once the original caller has given up — [gRPC](https://grpc.io/docs/guides/deadlines/) sends it on the wire, but each server must pass it on to its outbound calls and honor cancellation. - Retries, backoff, and circuit breakers: see [distributed.md](distributed.md). ### Schema evolution -- **Backward compatible**: new readers read old data; **forward compatible**: old readers read new data — rolling deploys and replayed events need both. -- Protobuf: never reuse or renumber a field tag (mark removed ones `reserved`); adding fields is safe; proto3 has no `required`; unknown fields survive a round trip. -- Avro with a schema registry checks BACKWARD, FORWARD, or FULL compatibility on every schema change. -- JSON consumers must ignore unknown fields (**tolerant reader**); rename a field by adding the new one, writing both, moving readers over, then removing the old. +- [Backward compatible](https://en.wikipedia.org/wiki/Backward_compatibility): new readers read old data; [forward compatible](https://en.wikipedia.org/wiki/Forward_compatibility): old readers read new data — rolling deploys and replayed events need both. +- [Protobuf](https://protobuf.dev/programming-guides/proto3/#updating): never reuse or renumber a field tag (mark removed ones [`reserved`](https://protobuf.dev/programming-guides/proto3/#reserved)); adding fields is safe; proto3 has no `required`; unknown fields survive a binary round trip, but not conversion to JSON. +- [Avro](https://avro.apache.org/docs/) with a [schema registry](https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html) checks BACKWARD, FORWARD, or FULL compatibility on every schema change. +- JSON consumers must ignore unknown fields ([tolerant reader](https://martinfowler.com/bliki/TolerantReader.html)); rename a field by adding the new one, writing both, moving readers over, then removing the old. ### Webhooks -- **Sign** each payload (HMAC over timestamp + body) and have receivers verify it with a **constant-time** compare; reject stale timestamps to stop replays. -- Deliver **at least once** with backoff, so receivers dedupe by event ID; receivers should ack fast (2xx) and process asynchronously. -- Fetching a user-supplied webhook URL is an SSRF risk — see [security.md](security.md). +- Sign each payload ([HMAC](https://en.wikipedia.org/wiki/HMAC) over timestamp + body) and have receivers verify it with a [constant-time](https://en.wikipedia.org/wiki/Timing_attack) compare; reject stale timestamps to stop replays. +- Deliver at least once with backoff, so receivers dedupe by event ID; receivers should ack fast (2xx) and process asynchronously. +- Fetching a user-supplied webhook URL is an [SSRF](https://community.owasp.org/attacks/Server_Side_Request_Forgery) risk — see [security.md](security.md). ## Async work ### Long-running operations -- Return **202 Accepted** with a `Location` to a status resource (`/operations/123`); the client polls it or gets a webhook when done. -- The status resource carries state (`pending`, `running`, `succeeded`, `failed`) and the result or error. +- Return [202 Accepted](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/202) with a [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Location) to a status resource (`/operations/123`); the client polls it or gets a webhook when done. +- The status resource carries state (`pending`, `running`, `succeeded`, `failed`) and the result or error ([asynchronous request-reply](https://learn.microsoft.com/en-us/azure/architecture/patterns/asynchronous-request-reply)). ### Background jobs -- At-least-once delivery means jobs must be **idempotent**; persistently failing jobs go to a **dead-letter queue** with alerting. +- At-least-once delivery means jobs must be [idempotent](https://en.wikipedia.org/wiki/Idempotence); persistently failing jobs go to a [dead-letter queue](https://en.wikipedia.org/wiki/Dead_letter_queue) with alerting. - Give every job a timeout; long jobs checkpoint progress so a retry resumes instead of restarting. ### Scheduled jobs -- Guard against **overlapping runs**, where one run lasts longer than the interval. -- With several replicas, each fires the same cron — use a lock (DB row, advisory lock, lease) or a single scheduler. +- Guard against overlapping runs, where one run lasts longer than the interval. +- With several replicas, each fires the same [cron](https://en.wikipedia.org/wiki/Cron) — use a lock (DB row, [advisory lock](https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS), [lease](https://en.wikipedia.org/wiki/Lease_%28computer_science%29)) or a single scheduler. ## Data access ### N+1 queries -- Loading a list, then one query per item. Fix with a `JOIN`, a batched `WHERE id IN (...)`, or a **DataLoader** that batches lookups per tick (GraphQL resolvers). -- Spot it with per-request query counts in logs or APM. +- Loading a list, then one query per item. Fix with a `JOIN`, a batched `WHERE id IN (...)`, or a [DataLoader](https://github.com/graphql/dataloader) that batches lookups per tick (GraphQL resolvers). +- Spot it with per-request query counts in logs or [APM](https://en.wikipedia.org/wiki/Application_performance_management). ### Connection pool sizing -- Size the pool from the **database's** capacity, not the app's thread count — many app instances × a large pool overwhelm the database. -- A good starting point is a small pool per instance (~2× DB cores total across instances), then measure wait time. +- Size the [connection pool](https://en.wikipedia.org/wiki/Connection_pool) from the database's capacity, not the app's thread count — many app instances × a large pool overwhelm the database. +- A good starting point is a small pool per instance (~2× DB cores total across instances, [HikariCP's formula](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing)), then measure wait time; see [database.md](database.md). ### Transaction boundaries -- Never make a network call inside an open DB transaction — it holds **locks and a connection** for the whole round trip. -- For "commit, then notify another service", use the outbox instead of calling inside the transaction. +- Never make a network call inside an open DB transaction — it holds locks and a connection for the whole round trip. +- For "commit, then notify another service", use the [outbox](https://microservices.io/patterns/data/transactional-outbox.html) instead of calling inside the transaction. ### Multi-tenancy models -- Shared tables with a `tenant_id`: cheapest, but every query must filter — enforce it with PostgreSQL **row-level security** (the table owner bypasses it unless `FORCE ROW LEVEL SECURITY`). +- Shared tables with a `tenant_id`: cheapest, but every query must filter — enforce it with PostgreSQL [row-level security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) (the table owner bypasses it unless [`FORCE ROW LEVEL SECURITY`](https://www.postgresql.org/docs/current/sql-altertable.html)). - Schema per tenant: more isolation, but migrations and connection pools multiply with the tenant count. - Database per tenant: strongest isolation, per-tenant restore and data residency, the most operational work. -- Move **noisy neighbors** (the largest tenants) to their own shard or database, and rate-limit per tenant. +- Move [noisy neighbors](https://learn.microsoft.com/en-us/azure/architecture/antipatterns/noisy-neighbor/noisy-neighbor) (the largest tenants) to their own shard or database, and rate-limit per tenant. ### Time handling -- Store and send instants in **UTC** (RFC 3339 with an offset, `2026-03-08T09:30:00Z`); in PostgreSQL use `timestamptz`. -- For future local-time events (meetings, schedules), store the local time plus an **IANA zone** (`Europe/Berlin`), not an offset — DST rules and offsets change. -- A DST switch skips or repeats a local hour, so a job scheduled at 02:30 local time runs never or twice; schedule jobs in UTC. +- Store and send instants in UTC ([RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) with an offset, `2026-03-08T09:30:00Z`); in PostgreSQL use [`timestamptz`](https://www.postgresql.org/docs/current/datatype-datetime.html). +- For future local-time events (meetings, schedules), store the local time plus an [IANA zone](https://www.iana.org/time-zones) (`Europe/Berlin`), not an offset — DST rules and offsets change. +- A [DST](https://en.wikipedia.org/wiki/Daylight_saving_time) switch skips or repeats a local hour, so a job scheduled at 02:30 local time runs never or twice; schedule jobs in UTC. ### File uploads -- Clients upload straight to object storage with a presigned URL, bypassing app servers ([system.md](system.md)). -- **Multipart/resumable uploads** retry individual parts instead of the whole file. +- Clients upload straight to object storage with a [presigned URL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html), bypassing app servers ([system.md](system.md)). +- [Multipart](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html) or [resumable](https://tus.io/protocols/resumable-upload) uploads retry individual parts instead of the whole file. ## Service lifecycle ### Graceful shutdown -- On **`SIGTERM`**: fail readiness → stop accepting new connections → finish in-flight requests within a deadline → close pools and flush telemetry → exit. +- On [`SIGTERM`](https://man7.org/linux/man-pages/man7/signal.7.html): fail readiness → stop accepting new connections → finish in-flight requests within a deadline → close pools and flush telemetry → exit. - Workers stop pulling jobs and finish or checkpoint the current one. Kubernetes timing: see [devops.md](devops.md). ### Health endpoints -- Separate endpoints: **liveness** checks only that the process can make progress; **readiness** checks what serving needs (warm-up done, required dependencies) and fails while draining. +- Separate endpoints: [liveness](https://kubernetes.io/docs/concepts/workloads/pods/probes/) checks only that the process can make progress; readiness checks what serving needs (warm-up done, required dependencies) and fails while draining. - Keep them cheap and on an internal port; probe configuration and pitfalls: see [devops.md](devops.md). ## Backend security ### Mass assignment -- Binding the request body straight onto a model lets clients set `isAdmin: true` — **allowlist** bindable fields or use separate DTOs. +- Binding the request body straight onto a model lets clients set `isAdmin: true` ([mass assignment](https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html)) — allowlist bindable fields or use separate [DTOs](https://martinfowler.com/eaaCatalog/dataTransferObject.html). - Serialize responses through explicit DTOs too, so a new sensitive column doesn't leak. ### Input validation -- Validate type, format, and range at the boundary before business logic (**400**); business-rule failures come later (409/422). -- Cap sizes — body, array lengths, string lengths, nesting depth — to prevent resource exhaustion. +- [Validate](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html) type, format, and range at the boundary before business logic (400); business-rule failures come later (409/422). +- Cap sizes — body, array lengths, string lengths, nesting depth — to prevent [resource exhaustion](https://api-security.owasp.org/editions/2023/en/0xa4-unrestricted-resource-consumption/). ### Secrets in logs -- Request logging middleware can capture tokens, auth headers, and PII — **redact centrally** in the logging layer, not per call site. +- Request logging middleware can capture tokens, auth headers, and [PII](https://en.wikipedia.org/wiki/Personal_data) — redact centrally in the logging layer, not per call site ([OWASP logging](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html)). - Tokens in URL query strings end up in proxy and CDN logs — send them in headers. ## Testing ### Integration tests with real dependencies -- **Testcontainers** runs real dependencies (Postgres, Kafka) in tests, catching SQL and serialization bugs that mocks hide. -- A mock only checks your assumptions about a dependency; test the real one at least at the adapter boundary. +- [Testcontainers](https://testcontainers.com/) runs real dependencies (Postgres, Kafka) in tests, catching SQL and serialization bugs that mocks hide. +- A [mock](https://martinfowler.com/articles/mocksArentStubs.html) only checks your assumptions about a dependency; test the real one at least at the adapter boundary. ### Consumer-driven contract tests -- **Consumer-driven contract tests** (Pact): each consumer records the requests and fields it relies on, and the provider's CI verifies them. +- [Consumer-driven contract tests](https://martinfowler.com/articles/consumerDrivenContracts.html) ([Pact](https://docs.pact.io/)): each consumer records the requests and fields it relies on, and the provider's CI verifies them. - Catches breaking API changes without deploying both services together. diff --git a/docs/topics/frontend.md b/docs/topics/frontend.md index 01abce6..fb4665c 100644 --- a/docs/topics/frontend.md +++ b/docs/topics/frontend.md @@ -8,69 +8,69 @@ What experienced frontend engineers forget before an interview, grouped by subto | Strategy | Renders | Trade-off | |---|---|---| -| CSR | in the browser after JS loads | slow first paint and SEO, fast later navigation | -| SSR | on the server, per request | fresh and SEO-friendly, higher server load | -| SSG | at build time | fastest and cheapest, stale until rebuild | -| ISR | like SSG, regenerated in the background after a revalidate interval or on demand | near-CDN speed; serves stale until regeneration succeeds | +| [CSR](https://web.dev/articles/rendering-on-the-web#client-side) | in the browser after JS loads | slow first paint and SEO, fast later navigation | +| [SSR](https://web.dev/articles/rendering-on-the-web#server-side) | on the server, per request | fresh and SEO-friendly, higher server load | +| [SSG](https://web.dev/articles/rendering-on-the-web#static) | at build time | fastest and cheapest, stale until rebuild | +| [ISR](https://nextjs.org/docs/app/guides/incremental-static-regeneration) | like SSG, regenerated in the background after a revalidate interval or on demand | near-CDN speed; serves stale until regeneration succeeds | ### Streaming SSR and hydration -- **Streaming SSR** flushes HTML in chunks, so the shell paints before slow data-dependent sections resolve. -- **Hydration** runs the render on the client and attaches listeners to the server HTML, reusing the DOM instead of recreating it. -- A **hydration mismatch** (server and client render different output) can force the framework to re-render that part on the client — React's causes and fixes in [react.md](react.md). +- [Streaming SSR](https://react.dev/reference/react-dom/server/renderToPipeableStream) flushes HTML in chunks, so the shell paints before slow data-dependent sections resolve. +- [Hydration](https://en.wikipedia.org/wiki/Hydration_%28web_development%29) runs the render on the client and attaches listeners to the server HTML, reusing the DOM instead of recreating it. +- A hydration mismatch (server and client render different output) can force the framework to re-render that part on the client — React's causes and fixes in [react.md](react.md). ### Islands and Server Components -- **Islands architecture**: the page is static HTML; only interactive islands ship JS and hydrate independently. -- **React Server Components** render only on the server and send serialized output, not code — only Client Components ship JS and hydrate (see [react.md](react.md)). +- [Islands architecture](https://docs.astro.build/en/concepts/islands/): the page is static HTML; only interactive islands ship JS and hydrate independently. +- [React Server Components](https://react.dev/reference/rsc/server-components) render only on the server and send serialized output, not code — only Client Components ship JS and hydrate (see [react.md](react.md)). ### Client-side routing -- The router intercepts navigation and updates the URL with the **History API** (`pushState`), no full reload. -- A refresh on `/profile` hits the **server**, which must fall back to `index.html` for unknown paths or it 404s. +- The router intercepts navigation and updates the URL with the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) ([`pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState)), no full reload. +- A refresh on `/profile` hits the server, which must fall back to `index.html` for unknown paths or it 404s. ## Browser pipeline ### Critical rendering path -- HTML → DOM, CSS → CSSOM → **render tree** (visible nodes) → layout → paint → composite. -- CSS is **render-blocking**; a classic `