# JavaScript Performance & Internals β Deep Dive
> V8 engine internals, JIT compilation, garbage collection, and performance optimization.
---
## 1. V8 Engine Architecture
V8 (Chrome/Node.js) is JavaScript's most influential engine. Here's how it works:
```
Source Code β Parser β AST β Ignition (Interpreter) β TurboFan (JIT Compiler) β Optimized Machine Code
β
Feedback from Deoptimization
```
### Two Compilers, Two Speeds
| Stage | Compiler | Speed | Output |
|-------|----------|-------|--------|
| First | **Ignition** | Fast startup | Bytecode (interpreted) |
| Second | **TurboFan** | Optimized | Native machine code |
**Ignition** interprets bytecode immediately β fast startup.
**TurboFan** compiles "hot" functions to native code β maximum performance.
### How Functions Become Optimized
```
Function called once β Ignition interprets
Function called 10x β Still interpreted (collecting feedback)
Function called ~1000x β TurboFan kicks in (magical number)
Function deoptimized β Falls back to Ignition
```
### Deoptimization β When Optimizations Break
```javascript
function add(x, y) {
return x + y; // TurboFan: "x and y are always numbers β use floating-point add"
}
add(1, 2); // Optimized
add("1", "2"); // β Deoptimized! TurboFan assumed numbers. Falls back to Ignition.
add(3, 4); // Interpreted (may re-optimize if called enough)
```
**Key lesson:** Consistent data types = faster code.
---
## 2. Hidden Classes (Maps)
V8 uses hidden "maps" to optimize object property access.
### How V8 Optimizes Objects
```javascript
// Constructor pattern β ALL instances share the same map
function Point(x, y) {
this.x = x; // Properties added in same order
this.y = y;
}
const a = new Point(1, 2);
const b = new Point(3, 4);
// β
a and b share the same hidden class β O(1) property access
// Object literal with extra property β different map!
const c = { x: 5, y: 6, z: 7 };
// β c has a different hidden class than a and b
// Adding properties later β triggers transition chain
const d = { x: 1 };
d.y = 2; // Map transition: {x} β {x,y}
d.z = 3; // Map transition: {x,y} β {x,y,z}
// Each transition creates a new hidden class
```
### Best Practices for V8 Optimization
```javascript
// β
Good β consistent shape
class User {
constructor(name, email, age) {
this.name = name; // Always same order
this.email = email;
this.age = age;
}
}
// β Bad β inconsistent shape
class UserBad {
constructor(name, email, age) {
this.name = name;
this.email = email;
if (age) { // Sometimes has 'age', sometimes doesn't
this.age = age;
}
}
}
// β
Good β delete doesn't change shape (use null/undefined)
user.age = null; // Keeps the same map
// β Bad β delete creates new map
delete user.age; // New transition!
```
---
## 3. Inline Caching
V8 caches property access patterns at each call site.
```javascript
const shapes = [
{ x: 1, y: 2, area() { return this.x * this.y; } },
{ x: 3, y: 4, area() { return this.x * this.y; } },
{ x: 5, y: 6, area() { return this.x * this.y; } },
];
// Monomorphic β all same shape β fastest
shapes.forEach(s => console.log(s.area()));
// V8 caches: "shape.area β function at address 0x..."
// Polymorphic β a few shapes β still fast
const mixed = [...shapes, { x: 7, y: 8, area() { return this.x * this.y * 2; } }];
// Megamorphic β many shapes β slow
const chaos = shapes.map((_, i) => ({ x: i, y: i, area() { return i * i; } }));
// Each has different map β V8 gives up on caching
```
---
## 4. Garbage Collection
### V8's Two-Space GC
```
Young Generation (Nursery) Old Generation
ββββββββββββ βββββββββββββββββββ
β 2-512MB β ββsurvivesβββ β Unbounded β
β Scavenge β β Mark-Sweep- β
β (fast) β β Compact β
ββββββββββββ βββββββββββββββββββ
```
### GC Generations
| Generation | Algorithm | Objects | Speed |
|-----------|-----------|---------|-------|
| **Young** | Scavenge (copying) | New objects, short-lived | Very fast (~ms) |
| **Old** | Mark-Sweep-Compact | Promoted objects, long-lived | Slower (~10s ms) |
### GC Generations β When Objects Promote
```javascript
// Object starts in Young Generation
const obj = { data: new Array(1000) };
// After 1-2 GC cycles, promoted to Old Generation
// If obj survives 2 scavenge cycles β promoted
// Force old generation GC (Node.js)
// node --expose-gc index.js
// globalThis.gc(); // Full GC
```
### Memory Leak Patterns
```javascript
// β Global variable leak
function leak() {
leakedData = new Array(1000000); // Implicit global!
}
// β Forgotten timer
let timer = setInterval(() => {
console.log('Still running...');
}, 1000);
// Never clearInterval β keeps callback + closure alive
// β Detached DOM elements
const element = document.createElement('div');
document.body.appendChild(element);
// ... later removed from DOM but referenced in variable
// element variable prevents GC
// β Closures holding large data
function processData(largeArray) {
const result = largeArray.reduce((sum, n) => sum + n, 0);
return () => {
console.log(result); // β
Only captures result, not largeArray
// If we referenced largeArray here, it would stay in memory
};
}
// β Event listeners on short-lived objects
element.addEventListener('click', handler);
// Element removed from DOM but listener still references it
```
### Detecting Memory Leaks
```javascript
// In DevTools Memory tab:
// 1. Take heap snapshot
// 2. Do action that might leak
// 3. Take another snapshot
// 4. Compare β look for growing object counts
// Programmatic check (Node.js)
import { performance } from 'perf_hooks';
function getMemoryUsage() {
const used = process.memoryUsage();
return {
rss: Math.round(used.rss / 1024 / 1024), // Resident set size (MB)
heapTotal: Math.round(used.heapTotal / 1024 / 1024),
heapUsed: Math.round(used.heapUsed / 1024 / 1024),
external: Math.round(used.external / 1024 / 1024),
};
}
console.log(getMemoryUsage());
// { rss: 50, heapTotal: 25, heapUsed: 18, external: 2 }
```
---
## 5. JavaScript Performance Anti-Patterns
### Synchronous Work on Main Thread
```javascript
// β Blocks UI for 3+ seconds
function processLargeData(data) {
const results = [];
for (let i = 0; i < data.length; i++) {
results.push(expensiveCalculation(data[i]));
}
return results;
}
// β
Split into chunks with microtask yield
async function processChunked(data, chunkSize = 100) {
const results = [];
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
results.push(...chunk.map(expensiveCalculation));
// Yield to browser β allows UI to update
await new Promise(resolve => setTimeout(resolve, 0));
}
return results;
}
// β
Offload to Web Worker
const worker = new Worker('./processor.js');
worker.postMessage(largeData);
worker.onmessage = (e) => updateUI(e.data);
```
### String Concatenation in Loops
```javascript
// β Creates N temporary strings
let result = '';
for (let i = 0; i < 10000; i++) {
result += 'item' + i + ',';
}
// β
Array.join β single allocation
const parts = [];
for (let i = 0; i < 10000; i++) {
parts.push('item', i);
}
const result = parts.join(',');
// β
StringBuilder pattern with array
const builder = [];
builder.push('<ul>');
items.forEach(item => builder.push(`<li>${item}</li>`));
builder.push('</ul>');
const html = builder.join('');
```
### DOM Manipulation
```javascript
// β Forces layout recalculation on every iteration
items.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
container.appendChild(div); // Triggers reflow each time
console.log(div.offsetTop); // Forces synchronous layout!
});
// β
DocumentFragment β single DOM insertion
const fragment = document.createDocumentFragment();
items.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
fragment.appendChild(div);
});
container.appendChild(fragment); // Single reflow
// β
Even better: innerHTML
container.innerHTML = items.map(item => `<div>${item}</div>`).join('');
// β
requestAnimationFrame for visual updates
requestAnimationFrame(() => {
container.innerHTML = generateHTML(items);
});
```
### Expensive DOM Queries in Loops
```javascript
// β Queries DOM on every iteration
for (let i = 0; i < items.length; i++) {
const list = document.getElementById('list'); // Query every time!
const li = document.createElement('li');
li.textContent = items[i];
list.appendChild(li);
}
// β
Query once
const list = document.getElementById('list');
for (let i = 0; i < items.length; i++) {
const li = document.createElement('li');
li.textContent = items[i];
list.appendChild(li);
}
```
---
## 6. Array Performance
### Method Comparison
```javascript
// For transforming data:
const arr = Array.from({ length: 1000000 }, (_, i) => i);
// β
.map() β optimized in V8
const doubled = arr.map(n => n * 2);
// β .forEach() + push β slower
const doubled2 = [];
arr.forEach(n => doubled2.push(n * 2));
// β
For loop β fastest in V8
const doubled3 = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
doubled3[i] = arr[i] * 2;
}
// .filter() β optimized
const evens = arr.filter(n => n % 2 === 0);
// .reduce() β slightly slower than for loop
const sum = arr.reduce((acc, n) => acc + n, 0);
// β
For loop sum β fastest
let sum2 = 0;
for (let i = 0; i < arr.length; i++) {
sum2 += arr[i];
}
```
### Object vs Map Performance
```javascript
// Small number of lookups β Object is fine
const dict = { a: 1, b: 2, c: 3 };
dict['a']; // Fast
// Dynamic keys β Map
const map = new Map();
map.set('key-with-hyphens', 1);
map.set(123, 2); // Non-string keys work!
map.set(null, 3); // Even null works!
map.has('key-with-hyphens');
map.get('key-with-hyphens');
// Large dataset with frequent lookups β Map
// O(1) guaranteed vs Object's potential O(n)
```
### Set vs Array for Membership Testing
```javascript
const items = Array.from({ length: 100000 }, (_, i) => i);
// β Array.includes β O(n)
items.includes(99999); // Scans entire array
// β
Set.has β O(1)
const itemSet = new Set(items);
itemSet.has(99999); // Instant
// Unique items
const unique = [...new Set([1, 2, 2, 3, 3, 3])]; // [1, 2, 3]
```
---
## 7. Debounce and Throttle
### Debounce β Wait for Idle
```javascript
// Run AFTER user stops doing the action for X ms
function debounce(fn, delay = 250) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// Usage β search input
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
fetchResults(e.target.value); // Only fires 250ms after typing stops
}, 300));
// With leading edge
function debounceWithLeading(fn, delay = 250) {
let timer;
let canRun = true;
return function(...args) {
if (!canRun) return;
fn.apply(this, args);
canRun = false;
clearTimeout(timer);
timer = setTimeout(() => { canRun = true; }, delay);
};
}
```
### Throttle β Limit Rate
```javascript
// Run AT MOST once every X ms
function throttle(fn, interval = 100) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
// Usage β scroll handler
window.addEventListener('scroll', throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100));
// Usage β resize handler
window.addEventListener('resize', throttle(() => {
recalculateLayout();
}, 150));
```
### rAF Throttle β Sync with Display Refresh
```javascript
// Throttle to 60fps (or whatever the display refresh rate is)
function rAFThrottle(fn) {
let ticking = false;
return function(...args) {
if (!ticking) {
requestAnimationFrame(() => {
fn.apply(this, args);
ticking = false;
});
ticking = true;
}
};
}
window.addEventListener('scroll', rAFThrottle(() => {
// Runs at most once per frame β perfectly synced with display
updateScrollProgress();
}));
```
---
## 8. Lazy Loading Patterns
### Lazy Module Loading
```javascript
// Load module only when needed
let heavyModule;
async function doHeavyWork() {
if (!heavyModule) {
heavyModule = await import('./heavy-module.js');
}
return heavyModule.processData(data);
}
// Route-based code splitting (React example)
const Dashboard = lazy(() => import('./Dashboard.jsx'));
const Settings = lazy(() => import('./Settings.jsx'));
```
### Lazy Image Loading
```javascript
// Native lazy loading
<img src="photo.jpg" loading="lazy" alt="...">
// IntersectionObserver for custom behavior
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
observer.unobserve(img);
}
});
}, {
rootMargin: '200px', // Start loading 200px before visible
threshold: 0.01,
});
document.querySelectorAll('[data-src]').forEach(img => observer.observe(img));
```
### Lazy Component Rendering
```javascript
// Render on first visibility
class LazyRender {
constructor(container, renderFn) {
this.container = container;
this.renderFn = renderFn;
this.rendered = false;
this.setupObserver();
}
setupObserver() {
this.observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !this.rendered) {
this.rendered = true;
this.renderFn(this.container);
this.observer.disconnect();
}
}, { rootMargin: '500px' });
this.observer.observe(this.container);
}
}
// Usage
new LazyRender(
document.getElementById('chart-container'),
(container) => renderChart(container, data)
);
```
---
## 9. Memory Optimization
### Object Pooling
```javascript
// Reuse objects instead of creating/destroying
class ObjectPool {
constructor(createFn, initialSize = 10) {
this.createFn = createFn;
this.pool = [];
for (let i = 0; i < initialSize; i++) {
this.pool.push(createFn());
}
}
acquire() {
return this.pool.pop() || this.createFn();
}
release(obj) {
this.pool.push(obj);
}
}
// Usage β particle system
const particlePool = new ObjectPool(
() => ({ x: 0, y: 0, vx: 0, vy: 0, active: false }),
100
);
function spawnParticle() {
const p = particlePool.acquire();
p.x = Math.random() * canvas.width;
p.y = Math.random() * canvas.height;
p.active = true;
return p;
}
function cleanupParticle(p) {
p.active = false;
particlePool.release(p); // Return to pool
}
```
### WeakMap for Caching
```javascript
// Cache that doesn't prevent garbage collection
const cache = new WeakMap();
function getMetadata(obj) {
if (cache.has(obj)) return cache.get(obj);
const metadata = expensiveAnalysis(obj);
cache.set(obj, metadata);
return metadata;
}
// When obj is no longer referenced elsewhere, cache entry is GC'd
```
### Transferable Objects
```javascript
// Transfer ArrayBuffer without copying
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
view.set([1, 2, 3, 4, 5, 6, 7, 8]);
// Transfer to worker (zero-copy)
worker.postMessage(buffer, [buffer]);
// buffer is now detached in main thread β cannot be read anymore
// Transfer between iframes
otherWindow.postMessage(buffer, [buffer]);
// PostMessage with transferable (ES2025)
// const newBuffer = buffer.transfer(16); // Grow to 16 bytes
```
---
## 10. V8-Specific Optimizations
### Smi (Small Integer) Range
V8 stores small integers (31-bit) directly, not as objects.
```javascript
// β
Smi β stored directly, fastest
const a = 42;
const b = -1000000;
// β Double β stored as heap object, slower
const c = 3.14;
const d = Number.MAX_SAFE_INTEGER + 1;
// β BigInt β stored as heap object, slowest
const e = 42n;
// β
Keep loop counters as Smi
for (let i = 0; i < 1000000; i++) { /* i is a Smi */ }
```
### Inline Optimizations
```javascript
// β
Simple functions β likely inlined
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
const result = add(multiply(3, 4), multiply(5, 6));
// V8 inlines all three β single machine instruction
// β Complex functions β unlikely to be inlined
function processData(a, b, c, d, e) {
// ... 100 lines of code
return result;
}
```
### Typed Arrays vs Regular Arrays
```javascript
// Regular array β each element is a JS object reference
const arr = [1, 2, 3, 4, 5];
// Memory: ~40 bytes per element + overhead
// Typed array β contiguous native memory
const typed = new Float64Array([1, 2, 3, 4, 5]);
// Memory: 8 bytes per element, no overhead
// Typed array is 5-10x faster for numerical computation
const N = 10000000;
// Regular array
const regular = new Array(N);
for (let i = 0; i < N; i++) regular[i] = i * 2;
// Typed array
const typed = new Float64Array(N);
for (let i = 0; i < N; i++) typed[i] = i * 2;
// Typed array: faster allocation, better cache locality
```
---
## 11. Bundle Size Optimization
### Tree Shaking
```javascript
// utils.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export function divide(a, b) { return a / b; }
// main.js β only imports add
import { add } from './utils.js';
// Bundler removes subtract, multiply, divide β
// Side effects must be declared
// package.json
{ "sideEffects": false } // Tells bundler all exports are pure
```
### Dynamic Import β Code Splitting
```javascript
// Split bundle into chunks
const routes = {
dashboard: () => import('./pages/Dashboard.jsx'),
settings: () => import('./pages/Settings.jsx'),
analytics: () => import('./pages/Analytics.jsx'),
};
// Load chunk only when user navigates
const component = await routes[page]();
```
---
## Quick Reference: Performance Tips
| Issue | Solution | Impact |
|-------|----------|--------|
| Jank on scroll | `rAFThrottle` + `transform` | High |
| Memory leak | WeakMap, remove listeners | High |
| Slow list rendering | Virtual scroll / Lazy render | High |
| Main thread blocked | Web Workers, chunked processing | High |
| Slow lookups | Map/Set instead of Object/Array | Medium |
| GC pauses | Object pooling, avoid large objects | Medium |
| Slow computation | Typed Arrays, keep values as Smi | Medium |
| Large bundle | Tree shaking, dynamic imports | Medium |
| Layout thrashing | Batch DOM reads/writes | High |
| Slow image load | `loading="lazy"`, srcset, WebP | Medium |
| Slow font load | `font-display: swap`, preload | Medium |
| Slow startup | Code splitting, lazy modules | Medium |
| Deoptimization | Consistent object shapes | Medium |
| Memory pressure | Transferable objects | Medium |