Skip to content
elephantoo

Performance & best practices

Lesson 34 of 34 16 min read

Write clean, fast code: immutability, avoiding layout thrashing, debouncing, linting and security basics.


You've learned the language, the browser and asynchronous code. This final lesson is about writing JavaScript like a professional: code that's readable and maintainable first, fast where it matters, and secure. These are the habits code reviewers look for.

Part 1: Readable, maintainable code#

Name things well

JavaScript
// ❌ What is d? What does 86400000 mean?
const d = (a, b) => Math.round((b - a) / 86400000);

// ✅ Self-explanatory
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const daysBetween = (start, end) => Math.round((end - start) / MS_PER_DAY);

console.log(daysBetween(new Date("2026-09-01"), new Date("2026-09-30"))); // 29
  • Variables are nouns (cartTotal), functions are verbs (calculateTotal), booleans are questions (isLoading, hasAccess).
  • Replace "magic numbers" with named constants.
  • Comments explain why, not what — the code shows what.

Small functions, early returns

JavaScript
// ❌ Deep nesting
function canCheckout(user, cart) {
  if (user) {
    if (user.verified) {
      if (cart.items.length > 0) {
        return true;
      }
    }
  }
  return false;
}

// ✅ Guard clauses
function canCheckoutClean(user, cart) {
  if (!user?.verified) return false;
  return cart.items.length > 0;
}

console.log(canCheckoutClean({ verified: true }, { items: [1] })); // true

Prefer const, immutability and pure functions

JavaScript
const cart = [{ name: "Book", price: 499 }, { name: "Pen", price: 49 }];

// ❌ Mutates shared state
function applyDiscountInPlace(items) {
  for (const item of items) item.price *= 0.9;
}

// ✅ Returns new data; the original stays intact
const applyDiscount = (items, rate) =>
  items.map((item) => ({ ...item, price: Math.round(item.price * (1 - rate)) }));

const discounted = applyDiscount(cart, 0.1);
console.log(cart[0].price, discounted[0].price); // 499 449

Pure functions (same input → same output, no side effects) are trivial to test and safe to reuse. Keep side effects — DOM updates, network calls, storage — at the edges of your program.

Let tools enforce consistency

  • Prettier formats code automatically — no more arguments about semicolons or quotes.
  • ESLint (or the faster Oxlint/Biome) catches bugs: unused variables, missing await, accidental globals, == comparisons.
  • TypeScript catches type errors before runtime.
  • Tests (Node's test runner, Vitest) prove your code works and keep it working.
  • Run them all in CI (e.g. GitHub Actions) on every pull request.
JSON
{
  "scripts": {
    "format": "prettier --write .",
    "lint": "eslint .",
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "check": "npm run lint && npm run typecheck && npm test"
  }
}

Handle errors deliberately

  • Validate inputs at the boundaries (forms, API responses).
  • Throw Error objects with useful messages; never swallow errors silently.
  • Show users a friendly message, and log details for developers.

Part 2: Performance#

"Premature optimisation is the root of all evil." — Donald Knuth. Write clear code first, measure, then optimise the part that's actually slow.

Measure first

JavaScript
console.time("build index");
const index = new Map();
for (let i = 0; i < 100_000; i++) index.set(`user-${i}`, { id: i });
console.timeEnd("build index"); // build index: 25ms (varies)

const t0 = performance.now();
index.get("user-99999");
console.log(`lookup took ${(performance.now() - t0).toFixed(3)} ms`);

In the browser, the Performance panel records exactly where time goes — scripting, layout, painting — and Lighthouse audits load performance. Watch the Core Web Vitals: LCP (how fast the main content appears), INP (how quickly the page responds to input) and CLS (how much the layout jumps).

Choose the right data structure

The biggest wins usually come from algorithms, not micro-tweaks:

JavaScript
const ids = Array.from({ length: 20_000 }, (_, i) => i);
const wanted = Array.from({ length: 20_000 }, (_, i) => i * 2);

console.time("array.includes");
const slow = wanted.filter((id) => ids.includes(id)); // O(n²): scans the array each time
console.timeEnd("array.includes");

console.time("set.has");
const idSet = new Set(ids);
const fast = wanted.filter((id) => idSet.has(id)); // O(n): constant-time lookups
console.timeEnd("set.has");

console.log(slow.length === fast.length); // true
Output
array.includes: 120ms
set.has: 1.5ms
true

(Timings vary by machine, but the gap is always dramatic.) Use Map/Set for lookups, and avoid nested loops over large arrays.

Don't block the main thread

  • Break long tasks into chunks and yield between them (see The event loop).
  • Move heavy computation (image processing, parsing big files) to a Web Worker:
JavaScript
// main.js
const worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });
worker.postMessage({ numbers: [5, 3, 9] });
worker.onmessage = (event) => console.log("Sum from worker:", event.data);

// worker.js
self.onmessage = (event) => {
  const sum = event.data.numbers.reduce((a, b) => a + b, 0);
  self.postMessage(sum);
};

Efficient DOM updates

DOM operations are much slower than plain JavaScript. The biggest trap is layout thrashing: interleaving writes and layout reads.

JavaScript
const boxes = document.querySelectorAll(".box");

// ❌ Each read after a write forces the browser to recalculate layout
boxes.forEach((box) => {
  box.style.width = "200px";      // write
  console.log(box.offsetHeight);  // read → forced reflow, every iteration
});

// ✅ Read everything first, then write everything
const heights = [...boxes].map((box) => box.offsetHeight);
boxes.forEach((box, i) => {
  box.style.height = `${heights[i] + 10}px`;
});

More DOM tips:

  • Build many elements in a DocumentFragment or use replaceChildren once, instead of inserting one by one.
  • Toggle a CSS class rather than setting many inline styles.
  • Animate transform and opacity (GPU-friendly), not top/left/width.
  • Use requestAnimationFrame for visual updates.
  • Use event delegation instead of thousands of listeners.
  • For very long lists, render only the visible rows ("virtualisation").
  • Use IntersectionObserver for lazy-loading and "infinite scroll" instead of scroll handlers.

Debounce and throttle busy events

JavaScript
function debounce(fn, wait) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), wait);
  };
}

function throttle(fn, interval) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn(...args);
    }
  };
}

const log = [];
const debounced = debounce((v) => log.push(`debounced:${v}`), 50);
const throttled = throttle((v) => log.push(`throttled:${v}`), 1000);

for (const v of [1, 2, 3]) {
  debounced(v);
  throttled(v);
}
setTimeout(() => console.log(log), 100); // [ 'throttled:1', 'debounced:3' ]
  • Debounce: run once, after the events stop (search boxes, auto-save, resize end).
  • Throttle: run at most once per interval while events continue (scroll position, drag, analytics).

Memoise expensive pure functions

JavaScript
function memoize(fn) {
  const cache = new Map();
  return (arg) => {
    if (!cache.has(arg)) cache.set(arg, fn(arg));
    return cache.get(arg);
  };
}

let calls = 0;
const slowSquare = (n) => {
  calls++;
  return n * n;
};
const fastSquare = memoize(slowSquare);
fastSquare(9);
fastSquare(9);
console.log(calls); // 1 – the second call came from the cache

Only memoise pure functions, and remember the cache uses memory.

Load less JavaScript

The fastest code is code you don't ship:

  • Use type="module" scripts or defer so scripts don't block rendering.
  • Code-split with dynamic import() so users download features only when needed.
  • Check bundle size (e.g. npx vite-bundle-visualizer) and prefer small, tree-shakeable libraries — often the platform already does it (fetch instead of an HTTP library, Intl instead of a date-formatting library, structuredClone instead of a deep-clone package).
  • Compress (gzip/Brotli), cache with long-lived headers, and lazy-load images with loading="lazy".

Avoid memory leaks

Long-running pages (single-page apps) leak memory when references stay alive:

  • Remove event listeners and clear intervals when a component or widget goes away (an AbortController makes this easy).
  • Don't keep growing global arrays or caches without limits.
  • Use WeakMap for data attached to objects you don't own.
  • Use the DevTools Memory panel to compare heap snapshots.

Part 3: Security basics#

  • Never insert untrusted data with innerHTML. Use textContent, or a sanitiser such as DOMPurify if you truly need HTML. This prevents XSS.
  • Never use eval() or new Function() with user input.
  • Don't put secrets in front-end code — API keys in your bundle are public.
  • Don't store auth tokens in localStorage; prefer HttpOnly, Secure, SameSite cookies.
  • Validate on the server, always — client-side validation is just UX.
  • Keep dependencies updated and run npm audit; fewer dependencies means less risk.
  • Use HTTPS everywhere and a Content Security Policy header to limit what scripts can run.

A checklist for every pull request#

  • Clear names, small functions, no dead code or stray console.log/debugger
  • const by default, === everywhere, no mutation of shared data
  • Errors handled; loading and error states in the UI
  • Inputs validated; no innerHTML with user data
  • Lint, type-check and tests pass
  • Measured before optimising; no obvious O(n²) on large data
  • Listeners, timers and requests cleaned up
  • Accessible: real buttons, labels on inputs, keyboard support

Common mistakes#

  • Micro-optimising (for vs forEach) while shipping a 2 MB bundle.
  • Optimising without measuring.
  • Clever one-liners that nobody (including future you) can read.
  • Copy-pasting code instead of extracting a function.

What's next#

🎉 Congratulations — you've completed the JavaScript course! You can now write modern JavaScript, build interactive pages, talk to APIs, use Node and npm, and add types with TypeScript. The natural next step is the React course, where you'll use everything you've learned here — especially functions, destructuring, array methods, immutability, modules and async/await — to build component-based user interfaces.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What causes "layout thrashing"?

  2. 2.What's the main difference between debouncing and throttling?

  3. 3.Before optimising code that feels slow, what should you do first?

Finished reading?

Mark this lesson complete to track your progress.