|$ curl https://forge-ai.dev/api/markdown?path=docs/js/performance
$cat docs/javascript-—-performance.md
updated This week·30 min read·published

JavaScript — Performance

JavaScriptPerformanceAdvancedAdvanced
Introduction

JavaScript performance optimization is about writing code that runs efficiently within the constraints of the browser — limited CPU, memory, and battery life. Modern JavaScript engines (V8, SpiderMonkey, JavaScriptCore) are highly optimized, but certain patterns can still trigger performance bottlenecks: excessive DOM manipulation, memory leaks, layout thrashing, and blocking the event loop with heavy synchronous work.

Performance optimization follows the 80/20 rule: 80% of the impact comes from 20% of optimizations. The key is measuring before optimizing. Premature optimization adds complexity without measurable benefit. This guide covers the highest-impact areas: rendering performance, memory management, network optimization, and bundle size reduction.

Performance is not just about speed — it encompasses perceived performance (how fast the user feels the page responds), memory efficiency, battery impact, and Core Web Vitals (LCP, FID, CLS). A performant application respects the user's device resources and provides a smooth, responsive experience.

performance-measurement.js
JavaScript
1// Performance measurement basics
2// console.time — simple, synchronous timing
3console.time('operation');
4heavyComputation();
5console.timeEnd('operation'); // "operation: 142.5ms"
6
7// Performance API — high-resolution timing
8const start = performance.now();
9heavyComputation();
10const duration = performance.now() - start;
11console.log(`Operation took ${duration.toFixed(2)}ms`);
12
13// Performance marks and measures
14performance.mark('start-task');
15await asyncOperation();
16performance.mark('end-task');
17performance.measure('task-duration', 'start-task', 'end-task');
18const entries = performance.getEntriesByName('task-duration');
19console.log(`Async task: ${entries[0].duration.toFixed(2)}ms`);
20
21// Detect long tasks (blocking main thread > 50ms)
22const observer = new PerformanceObserver((list) => {
23 list.getEntries().forEach((entry) => {
24 console.warn(`Long task detected: ${entry.duration}ms`);
25 });
26});
27observer.observe({ type: 'longtask', buffered: true });
Measuring Performance

You cannot optimize what you cannot measure. The browser provides a rich set of APIs to measure performance at every level — from individual function timing to full page load lifecycle. The Performance API is the foundation, supplemented by PerformanceObserver for monitoring specific metrics, and performance.memory (Chrome only) for memory profiling.

MetricAPITarget
LCPLargest Contentful Paint< 2.5s
FIDFirst Input Delay< 100ms
CLSCumulative Layout Shift< 0.1
TBTTotal Blocking Time< 200ms
TTFBTime to First Byte< 800ms
INPInteraction to Next Paint< 200ms
web-vitals.js
JavaScript
1// Core Web Vitals measurement
2function observeWebVitals() {
3 // Largest Contentful Paint
4 const lcpObserver = new PerformanceObserver((list) => {
5 const entries = list.getEntries();
6 const lastEntry = entries[entries.length - 1];
7 console.log(`LCP: ${lastEntry.startTime.toFixed(0)}ms`);
8 });
9 lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
10
11 // First Input Delay
12 const fidObserver = new PerformanceObserver((list) => {
13 list.getEntries().forEach((entry) => {
14 console.log(`FID: ${entry.processingStart - entry.startTime}ms`);
15 });
16 });
17 fidObserver.observe({ type: 'first-input', buffered: true });
18
19 // Cumulative Layout Shift
20 const clsObserver = new PerformanceObserver((list) => {
21 let cls = 0;
22 list.getEntries().forEach((entry) => {
23 if (!entry.hadRecentInput) cls += entry.value;
24 });
25 console.log(`CLS: ${cls.toFixed(3)}`);
26 });
27 clsObserver.observe({ type: 'layout-shift', buffered: true });
28}
29
30// Frame rate monitoring (for animations/rendering)
31let frameCount = 0;
32let lastFpsTime = performance.now();
33
34function measureFPS() {
35 frameCount++;
36 const now = performance.now();
37 if (now - lastFpsTime >= 1000) {
38 console.log(`FPS: ${frameCount}`);
39 frameCount = 0;
40 lastFpsTime = now;
41 }
42 requestAnimationFrame(measureFPS);
43}
44
45// Memory usage (Chrome only)
46if (performance.memory) {
47 setInterval(() => {
48 const { usedJSHeapSize, totalJSHeapSize, jsHeapSizeLimit } = performance.memory;
49 const usedMB = (usedJSHeapSize / 1048576).toFixed(1);
50 const totalMB = (totalJSHeapSize / 1048576).toFixed(1);
51 console.log(`Heap: ${usedMB}MB / ${totalMB}MB`);
52 }, 5000);
53}
🔥

pro tip

Use PerformanceObserver instead of polling performance.getEntries() — it is more efficient and provides real-time notifications. Always set { buffered: true } on the observer to catch entries that fired before the observer was registered. In production, use the web-vitals library for cross-browser normalization.
DOM Optimization

DOM operations are among the most expensive operations in browser JavaScript. Every read/write to the DOM can trigger layout recalculations (reflow) and repaints. The key optimization is to batch DOM reads together, then batch DOM writes together — avoiding layout thrashing where interleaved reads and writes force the browser to recalculate layout repeatedly.

dom-optimization.js
JavaScript
1// Layout thrashing — BAD: interleaved reads and writes
2function badPattern(elements) {
3 elements.forEach(el => {
4 const width = el.offsetWidth; // read (forces layout)
5 el.style.width = (width + 10) + 'px'; // write (invalidates layout)
6 const height = el.offsetHeight; // read (re-forces layout!)
7 el.style.height = (height + 10) + 'px'; // write
8 });
9}
10// Each iteration forces TWO layout recalculations
11
12// Batched — GOOD: separate reads from writes
13function goodPattern(elements) {
14 // Batch all reads first
15 const sizes = elements.map(el => ({
16 width: el.offsetWidth,
17 height: el.offsetHeight
18 }));
19 // Batch all writes after
20 elements.forEach((el, i) => {
21 el.style.width = (sizes[i].width + 10) + 'px';
22 el.style.height = (sizes[i].height + 10) + 'px';
23 });
24}
25// Only TWO layout recalculations total (one read batch, one write batch)
26
27// Document fragment — batch DOM insertions
28const fragment = document.createDocumentFragment();
29for (let i = 0; i < 1000; i++) {
30 const li = document.createElement('li');
31 li.textContent = `Item ${i}`;
32 fragment.appendChild(li);
33}
34list.appendChild(fragment); // single reflow instead of 1000
35
36// requestAnimationFrame — sync with paint cycle
37function animate() {
38 element.style.transform = `translateX(${x}px)`;
39 x += 1;
40 requestAnimationFrame(animate);
41}
42
43// Avoid forced reflows — common triggering properties
44// offsetTop, offsetLeft, offsetWidth, offsetHeight
45// scrollTop, scrollLeft, scrollWidth, scrollHeight
46// clientTop, clientLeft, clientWidth, clientHeight
47// getComputedStyle(), getBoundingClientRect()
48// All of these force the browser to compute layout immediately

info

The single biggest DOM performance mistake is reading layout properties inside loops that also write to the DOM. Use tools like fastdom or simply batch your reads and writes manually. For complex UI, consider virtual DOM frameworks (React, Vue) that batch updates automatically, or use display: none to detach elements before bulk modifications.
Memory Management

JavaScript uses automatic garbage collection (GC), but memory leaks still happen. Common leak patterns include forgotten event listeners, detached DOM references, closures holding large objects, global variables, and growing caches without bounds. Memory leaks cause progressive slowdowns, jank, and eventual crashes — especially in long-lived single-page applications.

Leak PatternCauseSolution
DOM leaksRemoved DOM nodes still referenced in JSNullify references on cleanup
Listener leaksEvent listeners not removed when element is removedUse AbortController or removeEventListener
Closure leaksClosures that retain large object referencesNullify unused references inside closures
Global leaksAccidental global variables (no strict mode)Use "use strict" and linting
Cache leaksUnbounded caches/Maps/SetsSet size limits, use WeakMap/WeakSet
Timer leakssetInterval not cleared when component unmountsAlways store and clear timer IDs
memory-management.js
JavaScript
1// WeakMap — keys can be garbage collected
2const cache = new WeakMap();
3
4function processData(obj) {
5 if (cache.has(obj)) {
6 return cache.get(obj);
7 }
8 const result = expensiveComputation(obj);
9 cache.set(obj, result);
10 return result;
11}
12// When obj is no longer referenced, WeakMap entry is GC'd automatically
13
14// WeakSet — similar for unique object tracking
15const processed = new WeakSet();
16
17function markProcessed(obj) {
18 processed.add(obj);
19}
20
21function isProcessed(obj) {
22 return processed.has(obj);
23}
24
25// AbortController — clean up event listeners
26function setupListeners() {
27 const controller = new AbortController();
28 const { signal } = controller;
29
30 window.addEventListener('resize', handleResize, { signal });
31 document.addEventListener('click', handleClick, { signal });
32
33 // Cleanup: controller.abort() removes ALL listeners at once
34 return () => controller.abort();
35}
36
37// Avoid detached DOM references
38function leakyComponent() {
39 const elements = [];
40 for (let i = 0; i < 1000; i++) {
41 const div = document.createElement('div');
42 document.body.appendChild(div);
43 elements.push(div);
44 }
45 // Later: document.body.innerHTML = '' — but elements array still holds refs!
46 // Fix: elements.length = 0; // Release all references
47}
48
49// Memory-efficient array handling
50// Avoid splice() on large arrays — it shifts all elements
51// Use pop()/push() for stack operations, shift()/unshift() sparingly
52// For stable arrays, use index marking instead of deletion
53
54// Object pooling — reuse objects instead of allocating new ones
55class ObjectPool {
56 constructor(factory, reset, initialSize = 10) {
57 this.factory = factory;
58 this.reset = reset;
59 this.pool = Array.from({ length: initialSize }, () => factory());
60 }
61
62 acquire() {
63 return this.pool.pop() || this.factory();
64 }
65
66 release(obj) {
67 this.reset(obj);
68 this.pool.push(obj);
69 }
70}

warning

Memory leaks are cumulative — a leak that grows by 1KB every second becomes 3.6MB after an hour. Use Chrome DevTools Memory tab to take heap snapshots before and after actions, identify detached DOM trees, and use the Allocation Timeline to spot functions that allocate excessive memory. Pay special attention to SPA navigation — component unmounts must clean up all subscriptions, timers, and DOM references.
Rendering Performance

Rendering performance determines how smoothly the page feels. The browser rendering pipeline — JavaScript, Style, Layout, Paint, Composite — must complete within 16.6ms (60fps) for smooth animations. Each step in the pipeline can become a bottleneck. The goal is to minimize or skip steps where possible: changing transform and opacity only triggers compositing (the cheapest step).

preview
rendering-performance.js
JavaScript
1// The rendering pipeline steps
2// 1. JavaScript → executes your code
3// 2. Style → recalculates CSS rules
4// 3. Layout → computes geometry (most expensive)
5// 4. Paint → fills in pixels
6// 5. Composite → layers are combined (GPU, cheapest)
7
8// Only trigger compositing when possible:
9// CSS properties that ONLY trigger composite:
10// transform, opacity
11// CSS properties that trigger layout + paint + composite:
12// width, height, margin, padding, top, left, border
13// font-size, line-height, display, position, float
14
15// requestAnimationFrame — sync with browser paint cycle
16function smoothAnimation(element) {
17 let start = performance.now();
18 const duration = 2000;
19
20 function animate(now) {
21 const elapsed = now - start;
22 const progress = Math.min(elapsed / duration, 1);
23 const eased = easeInOutCubic(progress);
24
25 element.style.transform = `translateX(${eased * 300}px)`;
26 element.style.opacity = 1 - eased * 0.5;
27
28 if (progress < 1) {
29 requestAnimationFrame(animate);
30 }
31 }
32
33 requestAnimationFrame(animate);
34}
35
36// Avoid layout thrashing — use will-change for frequently animated props
37element.style.willChange = 'transform';
38
39// Debounce expensive handlers (resize, scroll)
40function debounce(fn, ms = 100) {
41 let timer;
42 return (...args) => {
43 clearTimeout(timer);
44 timer = setTimeout(() => fn(...args), ms);
45 };
46}
47
48// Throttle for continuous events
49function throttle(fn, ms = 16) {
50 let last = 0;
51 return (...args) => {
52 const now = performance.now();
53 if (now - last >= ms) {
54 last = now;
55 fn(...args);
56 }
57 };
58}
59
60// Use passive event listeners for scroll/touch
61document.addEventListener('scroll', handler, { passive: true });
62// passive: true tells the browser you won't call preventDefault()
63// Allows the browser to optimize scrolling immediately

best practice

The rendering pipeline is the most critical performance concept in frontend development. Whenever you change a CSS property, ask yourself: "Does this trigger layout?" Stick to transform and opacity for animations. Use will-change to create promotion hints, and always prefer requestAnimationFrame over setInterval for visual updates.
Network Optimization

Network requests are often the biggest bottleneck in web application performance. Each request involves DNS lookup, TCP handshake (and TLS for HTTPS), latency, and bandwidth constraints. Optimizations focus on reducing the number of requests, minimizing payload size, and leveraging browser caching effectively.

network-optimization.js
JavaScript
1// Resource Hints — preload critical resources
2<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin />
3<link rel="preconnect" href="https://api.example.com" />
4<link rel="dns-prefetch" href="https://cdn.example.com" />
5<link rel="prefetch" href="/next-page.js" as="script" />
6
7// Programmatic preloading with JavaScript
8const link = document.createElement('link');
9link.rel = 'preload';
10link.as = 'image';
11link.href = 'hero-image.webp';
12document.head.appendChild(link);
13
14// Request batching — combine multiple requests
15class RequestBatcher {
16 constructor(url, maxBatch = 10, debounceMs = 50) {
17 this.url = url;
18 this.maxBatch = maxBatch;
19 this.debounceMs = debounceMs;
20 this.queue = [];
21 this.timer = null;
22 }
23
24 add(item) {
25 this.queue.push(item);
26 if (this.queue.length >= this.maxBatch) {
27 this.flush();
28 } else if (!this.timer) {
29 this.timer = setTimeout(() => this.flush(), this.debounceMs);
30 }
31 }
32
33 async flush() {
34 if (this.queue.length === 0) return;
35 clearTimeout(this.timer);
36 this.timer = null;
37
38 const batch = this.queue.splice(0, this.maxBatch);
39 try {
40 const response = await fetch(this.url, {
41 method: 'POST',
42 body: JSON.stringify({ items: batch }),
43 });
44 const results = await response.json();
45 return results;
46 } catch (err) {
47 console.error('Batch request failed:', err);
48 }
49 }
50}
51
52// Connection pooling — reuse connections with keep-alive
53// Enabled by default in HTTP/1.1 and HTTP/2
54// HTTP/2 allows multiplexing multiple requests over a single connection
55
56// Service Worker caching — offline-first strategy
57self.addEventListener('fetch', (event) => {
58 event.respondWith(
59 caches.match(event.request).then((cached) => {
60 return cached || fetch(event.request).then((response) => {
61 return caches.open('api-cache-v1').then((cache) => {
62 cache.put(event.request, response.clone());
63 return response;
64 });
65 });
66 })
67 );
68});
🔥

pro tip

HTTP/2 eliminates the need for domain sharding and sprite sheets by allowing multiplexed streams over a single connection. However, HTTP/2 push is being deprecated in Chrome — use 103 Early Hints and preload/preconnect resource hints instead. For data-heavy apps, implement incremental loading with pagination, virtual scrolling, or streaming.
Bundle Optimization

JavaScript bundle size directly impacts page load time — every kilobyte of JavaScript must be downloaded, parsed, compiled, and executed. Modern bundlers (webpack, Vite, esbuild, Rollup) provide tools to split code, tree-shake unused exports, and compress output. The goal is to ship only the code needed for the initial render and defer everything else.

TechniqueImpactTool/Method
Tree shakingRemoves unused exportsES modules + sideEffects: false
Code splittingLazy load route/page chunksimport() dynamic imports
MinificationRemoves whitespace, renames varsTerser, esbuild, swc
CompressionGzip/Brotli on the wireServer config (nginx, CDN)
Scope hoistingReduces module wrapper overheadwebpack's ModuleConcatenationPlugin
Dynamic importsDefer non-critical codeReact.lazy, dynamic import()
bundle-optimization.js
JavaScript
1// Dynamic imports — code splitting
2// Instead of: import { HeavyComponent } from './HeavyComponent';
3// Do this:
4const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
5
6// Usage with Suspense
7<React.Suspense fallback={<Loading />}>
8 <HeavyComponent />
9</React.Suspense>
10
11// Route-level code splitting (React Router)
12const Dashboard = React.lazy(() => import('./pages/Dashboard'));
13const Settings = React.lazy(() => import('./pages/Settings'));
14
15// Conditional imports — load on interaction
16button.addEventListener('click', async () => {
17 const { showChart } = await import('./chart-library');
18 showChart(data);
19});
20
21// Preload critical chunks while idle
22if ('requestIdleCallback' in window) {
23 requestIdleCallback(() => {
24 import('./heavy-analytics-module');
25 });
26}
27
28// Tree shaking — import only what you need
29// BAD: import { lodash } from 'lodash';
30// GOOD: import debounce from 'lodash/debounce';
31// Or better: import { debounce } from 'lodash-es';
32
33// Webpack analyzer configuration
34// Add to webpack.config.js:
35// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
36// plugins: [new BundleAnalyzerPlugin()]
37
38// Package.json optimization — check bundlephobia.com for package size
39// Prefer smaller alternatives:
40// date-fns (80KB) vs moment (230KB)
41// zustand (1KB) vs redux (12KB) + react-redux (7KB)
42// preact (3KB) vs react (42KB) — if you don't need synthetic events

info

Use tools like webpack-bundle-analyzer or vite-plugin-inspect to visualize your bundle and identify large dependencies. A good rule of thumb: keep the initial JavaScript bundle under 100KB (gzipped). Every additional 100KB of JavaScript adds roughly 1 second to page load time on average mobile connections.
Best Practices
Measure before optimizing — use Performance API, Lighthouse, and Web Vitals to identify real bottlenecks
Always batch DOM reads and writes separately to avoid layout thrashing
Use transform and opacity for animations — they only trigger compositing (GPU path)
Implement code splitting at route level — defer non-critical JavaScript until needed
Use passive event listeners for scroll/touch events to allow browser optimizations
Set reasonable cache policies with Service Workers for offline-first resilience
Monitor memory usage with heap snapshots — fix detached DOM trees and growing caches
Debounce expensive handlers (resize, scroll, input) and throttle continuous events (mousemove)
Use WeakMap/WeakSet for caches tied to object lifetimes — prevents accidental memory leaks
Audit dependencies regularly — remove unused packages, prefer smaller alternatives
🔥

pro tip

The single highest-impact performance optimization in most web apps is reducing JavaScript bundle size. JavaScript is the most expensive resource — it must be downloaded, parsed, compiled, and executed. Every kilobyte of JS costs more than an equivalent kilobyte of CSS or images. Audit your dependencies, use dynamic imports, and consider using ESBuild or SWC for faster builds.
$Blueprint — Engineering Documentation·Section ID: JS-PERFORMANCE·Revision: 1.0