Performance
Tree shaking and compression: removing what you don''''t use
A developer imports one function from lodash: import { debounce } from 'lodash'. The bundle analyzer shows 70 KB of lodash in the output. They expected 2 KB. The import looked right — but the module format was wrong.
How tree shaking works
Tree shaking is the bundler’s ability to detect and remove exports that are never imported. If a module exports 200 functions and the app imports only one, the other 199 are “dead code” — they should not appear in the bundle.
This works because ES module syntax (import / export) is statically analysable: the bundler can trace import chains at build time without executing code. CommonJS (require()) computes exports at runtime; the bundler cannot know statically which ones are used, so it includes everything.
// Tree-shakeable (ESM) — bundler sees at build time which exports are used
import { debounce } from 'lodash-es'; // Only debounce in the bundle
// NOT tree-shakeable (CJS) — bundler must include all exports
import { debounce } from 'lodash'; // All 70 KB of lodash in the bundleTo enable tree shaking: use the library’s ESM build (often listed as module in package.json), configure the bundler to use it (resolve.mainFields: ['module', 'main'] in Webpack, automatic in Vite), and ensure your own code uses named exports.
Four pitfalls that defeat tree shaking
Pitfall 1 — side effects at module scope. A module that sets window.foo = bar or registers a global when imported cannot be tree-shaken — the bundler must preserve the side effect even if no export is consumed. Fix: annotate the package as "sideEffects": false in package.json if it has no module-scope side effects, or use /* #__PURE__ */ annotations on specific calls.
Pitfall 2 — CommonJS re-export. Even if your code is ESM, a library that compiles to CJS cannot be tree-shaken. The bundler wraps the whole module. Check the library’s package.json: if there is no "module" or "exports" field pointing to an ESM build, the whole library ships. Switch to an ESM-first alternative or use cherry-picked imports (lodash/debounce instead of lodash).
Pitfall 3 — export * re-exports. export * from './module' re-exports everything. If even one consumer imports one export from this barrel, the entire module is kept. Fix: use explicit named re-exports in barrel files.
Pitfall 4 — dynamic import with a variable. await import('./module') is tree-shakeable; await import(name) where name is a runtime variable is not — the bundler cannot know which module will be requested at build time.
| Pitfall | Why it defeats tree shaking | Fix |
|---|---|---|
| Side effects at module scope | Bundler must preserve global mutation | sideEffects: false or #PURE annotation |
| CJS library | Exports computed at runtime | Use ESM build or cherry-pick imports |
| export * barrel files | One consumer keeps entire module | Explicit named re-exports |
| Dynamic import with variable | Module unknown at build time | Use string literals in import() |
Compression: gzip vs brotli
Compression reduces the bytes transferred over the wire. Text-based assets (JS, CSS, JSON, SVG) compress 60-80% with gzip and 70-90% with brotli. The same 500 KB uncompressed JS file becomes roughly 200 KB gzip’d or 160 KB brotli’d.
Important constraint: compression reduces download cost only. Parse, compile, and execute cost is determined by the uncompressed size of the code. Switching from gzip to brotli saves ~10-25% more on the wire — worth doing, but it does not address the CPU bottleneck.
Static vs dynamic compression: Static compression generates the compressed file at build time and serves it directly with Content-Encoding: br. Cost at request time: zero. Dynamic compression compresses on the fly per request: ~5-30 ms for brotli, ~1-5 ms for gzip. Senior pattern: static brotli for all static assets (JS, CSS); dynamic gzip for HTML responses that vary per user.
# Verify brotli is served
curl -sI -H "Accept-Encoding: br" https://your-app.com/app.js | grep content-encoding
# Expected: content-encoding: brWhy this works
Why does the uncompressed size determine parse cost? The browser decompresses the file before parsing. The JS engine receives and parses the original bytes, not the compressed form. Gzip and brotli are transport-layer optimisations — they save network bytes but the CPU work is the same.
A library ships only a CJS build. The team imports one utility function. What happens in the bundle?
A team switches from gzip to static brotli for their JS assets. Transfer size drops 15%. LCP improves 40 ms on mobile. Is the parse cost affected?
A module has `window.analytics = createAnalytics()` at module scope and is marked sideEffects: true. The app only imports one function from it. What happens?
- 01Why does importing from a CJS library ship the whole library even if you only use one function?
- 02A library is marked sideEffects: false but still appears fully in the bundle. What are two likely causes?
- 03What is the difference in effect between gzip and code splitting on parse cost?
Tree shaking relies on ESM static analysis to remove unused exports. The four pitfalls — CJS modules, module-scope side effects, barrel export*, and dynamic variable imports — silently defeat it and are worth auditing in every build. Compression (brotli preferred over gzip) reduces wire bytes but leaves parse cost unchanged. Together, tree shaking and compression complement code splitting: splitting reduces what a route downloads; tree shaking removes dead code within each chunk; compression shrinks the transfer. The next lesson covers third-party scripts — the category that most commonly blows through carefully set budgets.
appears again in159
- The journey of a request: seven stops from socket to responsejunior
- Accept and parse: from kernel queue to a typed requestmiddle
- Routing and middleware: choosing what runs, and in what ordermiddle
- Handler and response: from business logic to bytes on the wiremiddle
- Streaming and backpressure: when the client reads slower than you writesenior
- Timeouts and tail latency: budgets, deadlines, and the fan-out trapsenior
- Middleware and DI: the two patterns that shape every backendjunior
- Writing middleware: signatures, next(), and the three framework modelsmiddle
- Inversion of control: how dependencies reach a classmiddle
- DI scopes and lifecycles: singleton, request, transientmiddle
- DI as a testing seam: fakes, mocks, and the boundary that matterssenior
- DI containers in production: resolution graphs, circular deps, and when not tosenior
- Blocking vs non-blocking I/O: two ways to waitjunior
- The event loop: one thread, ordered phasesmiddle
- What blocks the loop: CPU work and sync callsmiddle
- Offloading CPU work: worker threads and the libuv poolmiddle
- Backpressure and bounded concurrencysenior
- Throughput under load: tail latency and saturationsenior
- Why pool: the cost of creating a connectionjunior
- Pool sizing: why bigger is not fastermiddle
- Acquisition and timeouts: the wait queue is the real latency dialmiddle
- Retry strategies: backoff, jitter, and thundering herdmiddle
- Observability, production failures, and global-scale designsenior
- Tasks, microtasks, and scheduler.yield()middle
- Timer accuracy, throttling, and idle workmiddle
- Node.js event loop: phases, nextTick, and loop lagsenior
- Rendering strategies: SSG, SSR, ISR, streaming, and hydrationjunior
- SSG, SSR, ISR, streaming, and RSC — how each worksmiddle
- Hydration cost: selective, progressive, islands, resumabilitymiddle
- Core Web Vitals: what LCP, INP, and CLS measurejunior
- LCP: four phases, one dominant costmiddle
- INP: input delay, processing, presentationmiddle
- Lab vs field: why the two disagree and how to use eachmiddle
- Metric tradeoffs, RUM attribution, and the CI+field loopsenior
- The full picture: URL to LCP to INP as a relay racejunior
- Eight layers traced: from the service worker to the second navigationmiddle
- Five canonical breaks: where production reliably diessenior
- The three-track method: reading traces and building a monitored systemsenior
- What an index is and how it speeds up queriesjunior
- The leading-column rule and composite index designmiddle
- Partial, expression, and covering indexesmiddle
- Index types: GIN, GiST, BRIN, Hash, Bloom, and HOT updatesmiddle
- Index-only scans, the Visibility Map, and INCLUDEsenior
- Production failure modes and the index audit playbooksenior
- Index design exercise: full-text search strategysenior
- EXPLAIN and execution plans: what the planner decides and whyjunior
- Scan types: Seq, Index, Bitmap, Index-Onlymiddle
- Join algorithms and the row-estimate cascademiddle
- pg_statistic, ANALYZE, and production observabilitymiddle
- Extended statistics: fixing correlated-column estimate failuressenior
- Plan cache, cost-constant tuning, and planner internalssenior
- Production failure modes and plan stabilitysenior
- Connection pools: amortising the cost of a Postgres backendjunior
- PgBouncer session, transaction, and statement modesmiddle
- Pool sizing: the (cores × 2) + spindles formula and the two-layer stackmiddle
- Pool exhaustion and idle-in-transaction: the 3 AM failure modemiddle
- Migrating to transaction mode: rollout playbook and PgBouncer 1.21 prepared statementsmiddle
- The Postgres process model and why raising max_connections degrades throughputsenior
- Pooler landscape 2026, serverless connection storms, and the full failure-mode taxonomysenior
- ADD COLUMN: instant in PG 11+ vs rewrite in older Postgresjunior
- The lock-queue failure mode: why instant DDL can freeze the databasemiddle
- Safe DDL patterns: NOT VALID, CONCURRENTLY, and unsafe-op fixesmiddle
- Migration failure taxonomy and production disciplinesenior
- Shard-key selection: hash, range, list, and directory strategiesmiddle
- Co-location and Citus: the invariant that makes sharding usablemiddle
- The hot-shard failure mode: detection, isolation, and durable policymiddle
- Online resharding, 2PC, and the operational cost of shardingsenior
- The seven acts: from CREATE TABLE to Citusjunior
- Acts 1–3 in depth: schema, indexes, and planner statisticsmiddle
- Acts 4–6 in depth: MVCC bloat, connection pooling, and safe migrationsmiddle
- Act 7 in depth: sharding, co-location, and the seven-tier tradeoff cascademiddle
- Observability, anti-patterns, and production triagesenior
- Bits on the wirejunior
- Latency mathmiddle
- Bufferbloat and congestionsenior
- The physical frontiersenior
- Sequence numbers and connection statemiddle
- Flow control and congestion controlmiddle
- BBR, production observability, and beyond TCPsenior
- CDN: putting content next doorjunior
- Anycast and GeoDNS: routing to the nearest edgemiddle
- Tiered cache and Cache-Controlmiddle
- Vary header and cache keysmiddle
- Stale-while-revalidate and cache stampedesenior
- Edge workers and edge-side compositionsenior
- CDN operations and observabilitysenior
- WebSocket: the HTTP upgrade handshakejunior
- WebSocket vs SSE vs long-polling: choosing the right transportmiddle
- WebSocket backpressure: when clients can''''t keep upmiddle
- Reconnection: jittered backoff, thundering herd, message resumptionsenior
- WebSocket at scale: HTTP/2 multiplexing, permessage-deflate, C10Msenior
- WebSocket in production: proxies, security, and distributed architecturesenior
- What reverse proxies dojunior
- Balancing algorithms: round-robin to power-of-two-choicesmiddle
- L4 vs L7 load balancing and client-IP preservationmiddle
- Health checks, connection draining, and slow startmiddle
- Retry storms, circuit breakers, and load sheddingsenior
- Resilient LB architecture: anycast, zone-aware routing, and observabilitysenior
- Why QUIC and not TCP+TLSjunior
- QUIC streams and head-of-line blockingjunior
- Integrated handshake and 1-RTTmiddle
- Connection IDs and network migrationmiddle
- Loss detection and congestion controlmiddle
- 0-RTT resumption and packet encryptionsenior
- Deployment tradeoffs and CPU costsenior
- DDoS: what it is and why it worksjunior
- Amplification attacks and state exhaustionmiddle
- Rate limiting: algorithms and architecturemiddle
- WAFs, firewalls, mTLS, and HSTSmiddle
- DNS cache poisoning and BGP hijackingsenior
- Defense-in-depth architecture and attack economicssenior
- The twelve layers: one URL, seven actorsjunior
- DNS, TCP, TLS in sequence: where the milliseconds gomiddle
- Critical render path and Core Web Vitalsmiddle
- Proxy intercepts and security gates: rate limiters, WAF, mTLSmiddle
- Alternate paths: QUIC 0-RTT, WebSocket upgrade, connection migrationmiddle
- Observability: distributed traces, USE/RED, and samplingsenior
- Resilience: cascading retries, circuit breakers, and error budgetssenior
- What the three signals are: logs, metrics, and tracesjunior
- Metrics and cardinality: the cost model of a time-series databasemiddle
- Logs and volume: the cost model of structured loggingmiddle
- Traces and sampling: the cost model of distributed tracingmiddle
- Join keys and exemplars: making the three signals composemiddle
- Observability 2.0: wide events and the cost shiftsenior
- Failure modes and engineering practice: cardinality budgets, PII, and samplingsenior
- Why structured logs exist: the diary vs the spreadsheetjunior
- The production log schema: fields every line must carrymiddle
- Log levels and alert routingmiddle
- Sampling strategies and log costmiddle
- PII redaction and log injectionsenior
- Trace context propagation in logssenior
- OTel Logs Data Model and audit logs as a subsystemsenior
- OTel signals, Semantic Conventions, and the OTLP wire formatmiddle
- Auto-instrumentation and manual spans: the 80/20 of OTelmiddle
- The OTel Collector: receivers, processors, exporters, and deployment patternsmiddle
- Sampling strategies: head, tail, and parent-basedmiddle
- Vendor neutrality, eBPF instrumentation, the Operator, and browser/serverless OTelsenior
- Operating the OTel Collector: reliability, version skew, failure modes, and governancesenior
- RED and USE: two checklists, one triage disciplinejunior
- Instrumenting RED in Prometheus: counters, histograms, and cardinality disciplinemiddle
- USE on Linux: CPU, memory, disk, network, and PSImiddle
- Golden signals, dashboard layout, and service mesh auto-REDmiddle
- Cardinality as a cost driver: labels, PII, exemplars, and samplingmiddle
- Native histograms, SLO tie-in, and production failure patternsmiddle
- Choosing SLIs and SLO targets: ratios, not feelingsmiddle
- Multi-window multi-burn-rate alerting: why AND beats ORmiddle
- Error budget policy, latency SLOs, and composite journeysmiddle
- Iceberg SLIs, composite SLO math, and SLA vs SLOsenior
- Flame graphs: reading the picture that shows where time goesjunior
- Sampling vs instrumentation profiling: why 99 Hz wins in productionmiddle
- Profile types: CPU, memory, off-CPU, mutex — which one to reach formiddle
- Continuous profiling: always-on flame graphs with eBPF and trace-id correlationmiddle
- How flame graphs are built from samples, and the production workflows that use themmiddle
- Linux perf, eBPF internals, PGO, and the limits of samplingsenior
- Profiling in production: security, war stories, OTel profiles, and the infrastructure designsenior
- The debugging funnel: SLO → RED → trace → profilejunior
- OTel architecture: one SDK, four signals, one wire formatmiddle
- Cost discipline: keeping observability under 5% of infra spendmiddle
- Scale, security, and the ROI of observable systemssenior