Skip to Content
QA Docs & Abangku 0.1.0 is released. Read more →
Product EngineerPerformance Guidelines

Performance Guidelines

12.1 Asset optimization

  • Images are served through the framework’s built-in image optimization (Next.js Image, see 2.2) rather than a raw <img> tag pointed at an unoptimized source. This gets automatic resizing, format negotiation (e.g. WebP/AVIF where supported), and lazy loading below the fold without hand-rolling any of it.
  • Fonts are loaded through the framework’s font optimization (next/font), self-hosted rather than fetched from a third-party CDN at request time, so font loading doesn’t add an external network dependency to page render.
  • Icons use the current stack’s icon library (lucide-react, see 2.2) as individually importable components, not a bundled icon-font or sprite sheet that ships every icon whether used or not.
  • Compression is enabled on every text-based response: API responses, HTML, CSS, and JavaScript, using brotli where the client supports it and gzip as the fallback, negotiated automatically via the standard Accept-Encoding mechanism. This is an application- layer or framework-level setting (the framework’s built-in compression, or a compression middleware in front of it), not an infrastructure/deployment concern, and it’s expected to be on by default, not something each service opts into individually.
  • Already-compressed formats are not compressed again. An image, a video, or another already-compressed binary format gains nothing from a second compression pass and wastes CPU doing it; compression applies to text-based payloads, where it has a real, measurable effect.

Rule: an unoptimized <img> tag, a directly-linked third-party font, or a service serving text-based responses without compression enabled is a review flag, not a style nitpick, all three have a direct, measurable cost on load performance that the framework already solves for free.

12.2 Bundle size budget

  • Budget: track client-side JavaScript bundle size per route and treat a meaningful regression (a new route or shared chunk growing significantly beyond its prior baseline) as something to justify in the PR, not something that passes silently.
  • Monitoring method: use the framework’s build output (Next.js’s build-time bundle analysis) as part of CI, so a bundle size regression is visible at PR time, not discovered later by a user on a slow connection.

Rule: a PR that adds a new dependency to a client bundle states, in the PR description, why that dependency needs to ship to the client at all (as opposed to running server-side) and roughly what it costs in bundle size. A dependency added without that justification is a candidate for the code-splitting rules below instead of a blanket import.

12.3 Code-splitting and lazy-loading

  • Heavy libraries are loaded on demand, not included in the initial bundle, if they’re only needed for a specific interaction: a charting library (Recharts, see 2.2) used on one dashboard tab, a PDF generation library (jsPDF, see 2.2) used only when a user clicks export.
  • Modals and off-screen content that require a heavy dependency are lazy-loaded so their cost isn’t paid by every visitor, only by the ones who actually open them.
  • Route-level code splitting is the default, provided by the framework; the additional discipline this document requires is at the component level, for the specific heavy-dependency cases above, since those aren’t automatically split just because they’re heavy.

Rule: a component that imports a heavy, interaction-gated dependency at the top of the file, instead of via a dynamic import triggered by that interaction, is paying the cost for every page load regardless of whether the feature is used. Treat that as a performance bug, not an acceptable default.

12.4 Query and database performance

Performance is not only a frontend concern; a slow query or a missing index is as much a performance regression as an unoptimized bundle, and is easy to omit if performance review only looks at the client.

  • Indexing strategy is deliberate, not accidental. A query pattern that will run frequently or against a growing table has a corresponding index, added and reviewed as part of the same change that introduces the query, not discovered later once the table is large enough to be slow.
  • N+1 query prevention is a backend authoring rule, defined in full in 6.6: never query inside a loop, batch-load related data instead. This subsection names it as a performance concern; 6.6 is where the concrete rule and review expectation live, don’t duplicate it here.
  • Pagination is required on unbounded collections, per 6.3 and 9.4: an endpoint that can return an ever-growing result set without a page limit is a performance problem waiting for the data to grow into it.

Rule: a new query against a table expected to grow past a small, fixed size needs an index answering “how does this query stay fast as the table grows,” decided at the time the query is written, not revisited only after it shows up in a slow-query log.

12.5 API response time targets

This is the backend equivalent of 5.20’s frontend Core Web Vitals targets: a stated latency goal for API responses, not just “as fast as reasonably possible” with no number to hold a change accountable to.

Stated default targets (tune per endpoint where a specific one genuinely needs a different bar, don’t apply these blindly to every endpoint regardless of what it does):

  • p95 under 500ms, p99 under 1.5s for an ordinary interactive request (a read, a simple write), measured at the API layer, not including client-side network latency the backend doesn’t control.
  • A long-running operation (per the 6.3 threshold, anything that wouldn’t run inline in a handler at all) is exempt from this target by definition, its latency is bounded by the background-job criteria in 10.4 instead, not the interactive-request target here.
  • Measure at p95/p99, not average. An average response time hides the tail: a fast median with a long tail of slow requests still produces a fast-looking average while a meaningful fraction of real users have a bad experience. p95/p99 is what actually reflects what the slower end of real traffic experiences.

Rule: a PR that introduces a new endpoint or measurably regresses an existing one past its stated p95/p99 target needs a justification in the PR description, the same standard already applied to a bundle-size regression in 12.2. A regression with no stated reason is a performance bug, not an acceptable tradeoff nobody noticed.

12.6 Load testing

Load testing validates that the system actually behaves the way its resilience design assumes it will under real concurrent load, not just that it works correctly for one request at a time in normal development and testing conditions.

  • Load test before a known traffic event, a launch, a marketing push, a known seasonal spike, not only reactively after a real incident already revealed a capacity problem. If a traffic increase is anticipated and sizeable, validate the system can handle it before it arrives, not during it.
  • What a load test should actually validate: that the bulkheading and resource isolation in 10.6 actually contains a degraded dependency’s impact under real concurrent load, that the backpressure and load-shedding behavior in 7.13 actually engages and rejects load predictably rather than degrading unpredictably, and that the response time targets in 12.5 above actually hold under the load level being tested, not just in isolation.
  • A load test against a shared or production environment is deliberate and coordinated, not run casually against infrastructure other traffic depends on. Prefer a dedicated environment sized similarly to production where practical; where that’s not practical, a production load test is scheduled and communicated, not sprung on the system unannounced.

Rule: a system-level change to resilience behavior (adjusting a bulkhead’s resource allocation, a backpressure threshold, a timeout budget) is validated with a load test before being trusted in production, the same way a claim about performance elsewhere in this document is expected to be measured, not assumed (8.14): a resilience mechanism that was never actually exercised under load is a hypothesis, not a verified guarantee.