Fundamentals of JavaScript — Interview Questions & Answers

50 essential JavaScript interview questions covering core concepts, ES6+, async programming, DOM manipulation, and modern patterns.

Meritshot24 min read
JavaScriptFrontendWeb DevelopmentInterview QuestionsES6
Back to Interview Guides

JavaScript Fundamentals

1. What is JavaScript and what is it used for?

JavaScript is a lightweight, interpreted, dynamic programming language that runs in the browser and on the server (via Node.js). Originally designed to add interactivity to web pages, it has evolved into a full-stack language used for frontend (React, Vue, Angular), backend (Node.js, Express), mobile (React Native), desktop (Electron), and serverless applications. JavaScript is the only language that runs natively in all browsers, making it essential for web development. It is single-threaded but uses an event loop and asynchronous APIs to handle concurrent operations.

2. What are the JavaScript data types?

JavaScript has eight data types. Primitive types (immutable, stored by value): string, number (both integer and float), bigint (arbitrary precision integers), boolean (true or false), undefined (declared but not assigned), null (intentional absence of value), and symbol (unique identifier). The one non-primitive type is object (includes arrays, functions, dates, and all other complex values) — stored by reference. typeof checks the type of a value, though typeof null incorrectly returns "object" due to a legacy bug in JavaScript.

3. What is the difference between var, let, and const?

var is function-scoped, hoisted (declared at the top of its function with value undefined), and can be re-declared. It is considered legacy and should be avoided. let is block-scoped, not re-declarable in the same scope, hoisted but not initialised (accessing before declaration throws a ReferenceError — the Temporal Dead Zone). const is block-scoped, cannot be re-assigned after initialisation (but objects and arrays it points to can be mutated), and must be initialised at declaration. Best practice: always use const by default; use let when re-assignment is needed; never use var.

4. What is hoisting in JavaScript?

Hoisting is JavaScript's behaviour of moving declarations to the top of their scope during the compilation phase before execution. var declarations are hoisted and initialised with undefined, so they can be accessed before their declaration line without a runtime error (though the value is undefined). Function declarations are fully hoisted — they can be called before they are defined. let and const are hoisted but not initialised, creating a Temporal Dead Zone (TDZ) where accessing them before their declaration throws a ReferenceError. Class declarations are also hoisted but have a TDZ.

5. What is the difference between == and ===?

== (loose equality) performs type coercion before comparing — it converts both operands to the same type if they differ. For example, 0 == false is true and null == undefined is true. This can produce surprising results and is a common source of bugs. === (strict equality) compares both value and type without coercion — 0 === false is false and null === undefined is false. Always use === and !== in modern JavaScript. The only common exception is x == null, which checks for both null and undefined simultaneously.

6. What is undefined vs null?

undefined means a variable has been declared but not yet assigned a value. It is the default value of uninitialised variables, missing function arguments, and missing object properties. null is an intentional assignment indicating "no value" or "empty" — it must be explicitly set. Both are falsy values. typeof undefined returns "undefined" while typeof null returns "object" (a legacy bug). Use null when you intentionally want to represent the absence of a value; undefined appears naturally when values are absent.

7. What is the difference between a function declaration and a function expression?

A function declaration defines a named function using the function keyword as a statement: function greet(name) { return "Hello " + name; }. It is fully hoisted — callable before it appears in the code. A function expression assigns a function to a variable: const greet = function(name) { return "Hello " + name; } or an arrow function: const greet = (name) => "Hello " + name. Function expressions are not hoisted. Arrow functions additionally do not have their own this, arguments, or prototype, making them unsuitable as constructors or methods requiring this context.

8. What is scope in JavaScript?

Scope determines where variables are accessible. Global scope: variables declared outside any function are accessible everywhere. Function scope: variables declared with var inside a function are accessible only within that function. Block scope: variables declared with let or const inside a block ({}) are accessible only within that block. Lexical scope (static scope): functions access variables from their enclosing scope at the time of definition, not at the time of execution. The scope chain is the series of nested scopes JavaScript searches when resolving a variable name.

9. What is the difference between null, undefined, and NaN?

null is an intentional absence of a value. undefined is an uninitialised or missing value. NaN (Not a Number) is a numeric value that results from invalid arithmetic operations like "text" / 2 or parseInt("hello"). NaN is the only value in JavaScript that is not equal to itself — NaN === NaN is false. Check for NaN using Number.isNaN(value) (strict, only true for actual NaN) or isNaN(value) (coerces value first). All three are falsy values but are distinct in meaning and type.

10. What are truthy and falsy values?

Falsy values are values that evaluate to false in a boolean context: false, 0, -0, 0n (BigInt zero), "" (empty string), null, undefined, and NaN. All other values are truthy, including "0", [] (empty array), {} (empty object), and function(){}. Truthy/falsy evaluation is used in conditional statements (if, ternary), logical operators (&&, ||), and short-circuit evaluation. The logical OR (||) returns the first truthy value; the nullish coalescing operator (??) returns the first non-null/non-undefined value — useful for default values.

ES6+ Features

11. What are arrow functions and how do they differ from regular functions?

Arrow functions provide a concise syntax: const add = (a, b) => a + b. Key differences from regular functions: (1) No own this — they inherit this from the enclosing lexical scope, making them ideal for callbacks but unsuitable as object methods or constructors; (2) No arguments object — use rest parameters instead; (3) Cannot be used as constructors (calling new throws); (4) No prototype property; (5) Implicit return when written without curly braces. Arrow functions are the preferred syntax for callbacks, array methods, and functional programming patterns.

12. What is destructuring?

Destructuring extracts values from arrays or objects into named variables. Array destructuring: const [first, second, ...rest] = [1, 2, 3, 4]. Object destructuring: const { name, age, city = 'Unknown' } = user (with default value). Nested destructuring: const { address: { street } } = user. Parameter destructuring: function greet({ name, role }) { ... }. Renaming: const { name: fullName } = user. Destructuring reduces repetitive property access, makes function parameters more explicit, and enables clean swap patterns: [a, b] = [b, a].

13. What are template literals?

Template literals use backticks (`) instead of quotes and support embedded expressions with ${expression} and multi-line strings without escape characters. For example: `Hello, ${user.name}! You have ${messages.length} messages.` evaluates expressions inline. Tagged template literals allow custom processing: html`<div>${content}</div>` where html is a function that receives the string parts and interpolated values. Template literals improve readability over string concatenation and are essential for generating HTML strings, SQL queries, and formatted output in JavaScript.

14. What is the spread operator and rest parameters?

The spread operator (...) expands an iterable into individual elements. For arrays: const combined = [...arr1, ...arr2] or Math.max(...numbers). For objects: const newObj = { ...obj1, ...obj2 } (shallow merge). Rest parameters collect multiple arguments into an array: function sum(...numbers) { return numbers.reduce((a, b) => a + b, 0) }. Spread and rest use the same syntax but operate in opposite directions — spread expands, rest collects. They replace older patterns like Array.prototype.slice.call(arguments) and concat() with cleaner, more readable code.

15. What are default parameters?

Default parameters allow function parameters to have default values when not provided or when undefined is passed: function greet(name = 'World', greeting = 'Hello') { return $, $!; }. Default values can be any expression — including function calls. Defaults are only applied for undefined (not null or 0). Earlier parameters can be referenced in later defaults: function createId(prefix = 'ID', separator = '-', value = prefix + separator + Date.now()) { ... }. Default parameters replace the older name = name || 'default' pattern, which incorrectly triggers for all falsy values.

16. What are modules in JavaScript?

ES6 modules allow splitting code into reusable files. Named exports: export const PI = 3.14; export function add(a, b) { return a + b; }. Default export: export default class Calculator { ... }. Importing: import { PI, add } from './math.js' for named, import Calculator from './calculator.js' for default. Dynamic import: const module = await import('./heavy.js') loads modules lazily. Modules are always in strict mode, have their own scope, and are singletons (the same module instance is shared across imports). CommonJS (require/module.exports) is the older Node.js module system.

17. What is optional chaining (?.)?

Optional chaining (?.) safely accesses deeply nested object properties without throwing a TypeError if an intermediate value is null or undefined — it short-circuits and returns undefined instead. user?.address?.street is equivalent to user && user.address && user.address.street. It works for method calls: user?.getName?.() and array indexing: arr?.[0]. Combined with nullish coalescing: user?.address?.city ?? 'Unknown' provides a clean default. Optional chaining eliminates verbose null checks and significantly reduces defensive coding boilerplate in modern JavaScript.

18. What is nullish coalescing (??)?

The nullish coalescing operator (??) returns the right-hand operand only when the left-hand operand is null or undefined. Unlike logical OR (||), it does not trigger for other falsy values like 0, "", or false. So 0 ?? 'default' returns 0, while 0 || 'default' returns 'default'. This makes ?? ideal for providing default values for optional configuration parameters or API responses where 0 or an empty string are valid values that should not be replaced. The ??= nullish assignment operator: x ??= 'default' assigns only if x is null or undefined.

19. What are symbols?

Symbols are a primitive type that creates unique, immutable identifiers: const id = Symbol('description'). Every Symbol call produces a guaranteed unique value — Symbol('id') !== Symbol('id'). Symbols are primarily used as unique object property keys that avoid naming collisions (e.g., when extending third-party objects or in library code): obj[Symbol('id')] = 1. Symbol properties are not included in for...in loops, Object.keys(), or JSON.stringify(). Symbol.iterator, Symbol.toPrimitive, and other well-known symbols customise built-in JavaScript behaviour.

20. What are WeakMap and WeakSet?

WeakMap stores key-value pairs where keys must be objects (not primitives) and are held weakly — if no other reference to the key exists, the entry is garbage collected. This prevents memory leaks when associating metadata with DOM nodes or objects that may be removed. WeakSet stores objects weakly. Neither is iterable and both have no size property, making them unsuitable for general-purpose use. Primary use case: caching or associating private data with objects without preventing garbage collection: const cache = new WeakMap(); cache.set(domElement, computedValue).

Async Programming

21. What is the event loop?

The JavaScript event loop is the mechanism that enables non-blocking, asynchronous behaviour in a single-threaded language. The call stack executes synchronous code. The Web API (browser) or libuv (Node.js) handles async operations (timers, HTTP requests, I/O) in separate threads. When an async operation completes, its callback is placed in the task queue (macrotask queue) or microtask queue (for Promises). The event loop continuously checks: if the call stack is empty, it dequeues the next microtask (all microtasks before the next macrotask), then one macrotask. This enables async programming without blocking the main thread.

22. What are Promises?

A Promise represents an asynchronous operation that will eventually resolve (succeed) or reject (fail). States: pending (initial), fulfilled (resolved with a value), rejected (failed with a reason). Created with new Promise((resolve, reject) => { ... }). Consumed with .then(onFulfilled, onRejected) or .catch(onRejected). Multiple Promises: Promise.all([p1, p2]) resolves when all resolve (or rejects when any rejects), Promise.allSettled([p1, p2]) always resolves with all results, Promise.race([p1, p2]) resolves/rejects with the first to settle, Promise.any([p1, p2]) resolves with the first to succeed.

23. What is async/await?

async/await is syntactic sugar built on top of Promises that makes asynchronous code look and behave synchronously. An async function always returns a Promise. await pauses execution of the async function until the Promise settles, then returns the resolved value (or throws for rejection). Error handling uses try/catch blocks. Parallel execution: const [a, b] = await Promise.all([fetchA(), fetchB()]) runs both concurrently rather than sequentially. await cannot be used at the top level of non-module scripts (though top-level await is supported in ES2022 modules).

24. What is the difference between microtasks and macrotasks?

Macrotasks (task queue) include setTimeout, setInterval, setImmediate, I/O callbacks, and UI rendering. Microtasks (microtask queue) include Promise callbacks (.then/.catch/.finally), queueMicrotask(), and MutationObserver. The event loop processes ALL microtasks after each macrotask and after the current call stack empties — microtasks have higher priority. This means: setTimeout(() => console.log('macro'), 0) runs after Promise.resolve().then(() => console.log('micro')) even though both are "asynchronous". Understanding this order is essential for debugging async execution order and avoiding subtle timing bugs.

25. What are callbacks and what is callback hell?

A callback is a function passed as an argument to another function, to be called when an async operation completes. Callbacks were the original async pattern: fs.readFile('file.txt', (err, data) => { ... }). Callback hell (pyramid of doom) occurs when multiple nested callbacks create deeply indented, hard-to-read code where error handling is repetitive and execution flow is non-linear. Promises flatten this chain, and async/await eliminates nesting entirely. Despite being considered legacy for complex async flows, callbacks are still used for event listeners, array methods (forEach, map, filter), and simple single-async-call scenarios.

26. What is a generator function?

Generator functions use the function* syntax and the yield keyword to produce a sequence of values lazily — one at a time, on demand. They return an iterator. When a generator function is called, it returns a generator object; execution only begins when .next() is called. Each .next() call runs until the next yield, returning { value, done }. Generators are useful for creating custom iterables, implementing infinite sequences, and managing complex async flows (before async/await). Redux-Saga uses generators for side effect management. yield* delegates to another iterable.

27. What is the difference between synchronous and asynchronous iteration?

Synchronous iteration uses the for...of loop with iterables that produce values synchronously (arrays, strings, generators). Asynchronous iteration uses for await...of with async iterables that produce Promises resolving to values over time — for example, reading chunks from a Node.js stream or consuming a web API paginated response lazily. Async iterables implement Symbol.asyncIterator. Async generators (async function*) combine both: they yield Promises and support await internally. for await...of must be used inside an async function or a module with top-level await.

28. What is the Fetch API?

The Fetch API is the modern browser-native way to make HTTP requests, replacing the older XMLHttpRequest. fetch(url, options) returns a Promise resolving to a Response object. response.json() parses the body as JSON (also returns a Promise). Error handling: fetch only rejects for network errors, not HTTP error codes (404, 500) — always check response.ok or response.status. AbortController cancels in-flight requests. Options include method, headers, body, credentials, and signal. In Node.js, Fetch is available natively since v18. Axios is a popular library that wraps Fetch/XHR with better error handling and interceptors.

29. What is CORS?

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that restricts web pages from making requests to a different domain, protocol, or port than the one that served the page (the same-origin policy). The server must include appropriate Access-Control-Allow-Origin headers to permit cross-origin requests. Simple requests (GET, POST with certain content types) send the request directly. Preflight requests (OPTIONS) are sent for complex requests (PUT, DELETE, custom headers) to check permissions. CORS is a server-side concern — developers configure it in their API server or CDN, not in the browser or fetch call.

30. What is localStorage vs. sessionStorage?

Both are Web Storage APIs for storing key-value pairs in the browser. localStorage persists across browser sessions (survives tab close and browser restart) and is shared across all tabs from the same origin. sessionStorage persists only for the duration of the browser tab — data is cleared when the tab closes and is not shared between tabs. Both store up to ~5MB of string data and are accessible via JavaScript. Neither should be used for sensitive data (tokens, personal data) as they are accessible to any JavaScript on the page. HTTP-only cookies are more secure for session tokens.

DOM & Browser

31. What is the DOM?

The DOM (Document Object Model) is a programming interface for HTML and XML documents that represents the page as a tree of objects, with the document object at the root. JavaScript uses the DOM API to dynamically read and modify content, structure, and styles. Key methods: document.getElementById('id'), document.querySelector('.class'), document.querySelectorAll('div'). Properties: element.textContent, element.innerHTML, element.style, element.classList. Events: element.addEventListener('click', handler). DOM manipulation is the foundation of interactive web applications.

32. What is event delegation?

Event delegation attaches a single event listener to a parent element instead of individual listeners on each child element. When an event occurs on a child, it bubbles up to the parent where it is handled. The target element is identified via event.target. For example, table.addEventListener('click', e => { if (e.target.tagName === 'TD') { ... } }) handles clicks on any table cell without attaching hundreds of listeners. Benefits: fewer event listeners (memory efficient), works for dynamically added elements, and simplifies cleanup. Event delegation is a fundamental performance pattern for lists, tables, and any container with many interactive children.

33. What is the difference between event.target and event.currentTarget?

event.target is the element that originally triggered the event — the actual element that was clicked or interacted with. event.currentTarget is the element to which the event listener is attached. These differ when event delegation is used: clicking a <span> inside a <button> sets event.target to the <span> and event.currentTarget to the <button> (where the listener is). event.stopPropagation() prevents the event from bubbling further up the DOM. event.preventDefault() prevents the default browser action (form submission, link navigation) without stopping propagation.

34. What is the virtual DOM?

The virtual DOM is an in-memory representation of the real DOM tree used by libraries like React. When state changes, React creates a new virtual DOM tree, compares it with the previous one (diffing/reconciliation algorithm), and calculates the minimal set of real DOM updates needed. This batch updating is more efficient than direct DOM manipulation for complex UIs because real DOM operations are expensive (triggering reflow and repaint). The virtual DOM itself is not faster than direct DOM manipulation — its benefit is in managing complex, frequently updating UIs by batching and optimising updates automatically.

35. What is debouncing and throttling?

Debouncing delays the execution of a function until a specified time has passed since the last invocation — useful for search boxes where you want to wait until the user stops typing before sending an API request: const debouncedSearch = debounce(search, 300). Throttling limits a function to execute at most once per specified interval — useful for scroll or resize events where you want regular (but not excessive) updates: const throttledHandler = throttle(handler, 100). Both prevent performance problems from rapid event firing. Lodash's _.debounce and _.throttle are the standard implementations.

Advanced Patterns

36. What is closure?

A closure is a function that has access to variables from its outer (enclosing) scope even after the outer function has returned. The inner function "closes over" the outer scope variables. Example: function makeCounter() { let count = 0; return () => ++count; } — the returned function retains access to count. Closures enable data privacy (encapsulation), factory functions, memoisation, and partial application. Every function in JavaScript is a closure (it retains a reference to its lexical scope). Common interview gotcha: var in loops creates one shared closure, while let creates a new binding per iteration.

37. What is prototypal inheritance?

JavaScript uses prototypal inheritance where objects inherit properties and methods from a prototype object. Every object has an internal [[Prototype]] link (accessible via Object.getPrototypeOf(obj) or obj.__proto__). When a property is accessed, JavaScript searches the object, then its prototype, then the prototype's prototype (the prototype chain), until reaching Object.prototype whose prototype is null. Constructor functions set the prototype of created objects via Constructor.prototype. ES6 class syntax is syntactic sugar over prototypal inheritance — extends sets up the prototype chain and super calls the parent constructor.

38. What is the difference between call(), apply(), and bind()?

All three set the this context for a function. func.call(thisArg, arg1, arg2) calls the function immediately with the specified this and individual arguments. func.apply(thisArg, [arg1, arg2]) calls the function immediately with this and arguments as an array. func.bind(thisArg, arg1) returns a new function with this permanently bound (and optionally pre-filled arguments) without calling it — useful for event handlers and callbacks. Example: setTimeout(obj.method.bind(obj), 1000) ensures this refers to obj inside method when the timeout fires.

39. What is memoisation?

Memoisation is an optimisation technique that caches the results of expensive function calls and returns the cached result when the same inputs occur again. A simple implementation: function memoize(fn) { const cache = {}; return (...args) => { const key = JSON.stringify(args); return key in cache ? cache[key] : (cache[key] = fn(...args)); }; }. Memoisation is effective for pure functions with expensive computations (Fibonacci, parsing, recursive algorithms). React's useMemo and useCallback hooks apply memoisation within components to avoid unnecessary re-renders and recalculations.

40. What is the difference between shallow and deep copy?

A shallow copy creates a new object or array with the same top-level property values as the original, but nested objects are still shared by reference. Methods: Object.assign({}, obj), spread operator { ...obj }, Array.from(arr), [...arr]. Modifying a nested object in the copy modifies the original. A deep copy recursively copies all nested objects so the new structure shares no references with the original. Methods: JSON.parse(JSON.stringify(obj)) (simple but fails for functions, Date, undefined, circular references), structuredClone(obj) (ES2022, handles more types), or libraries like Lodash _.cloneDeep().

41. What is this in JavaScript?

this refers to the context in which a function is executed, and its value depends on how the function is called. In the global scope: this is window (browser) or global (Node.js). As a method: this is the object before the dot. As a constructor: this is the newly created object. With call/apply/bind: this is explicitly set. In strict mode: this is undefined in plain function calls. Arrow functions: this is inherited from the enclosing lexical scope and cannot be changed with call/apply/bind. Understanding this is one of the most important and frequently misunderstood aspects of JavaScript.

42. What is the module pattern?

The module pattern uses closures to create private state and expose a public API. Classic IIFE pattern: const counter = (function() { let count = 0; return { increment: () => ++count, getCount: () => count }; })(). The count variable is private — inaccessible from outside. The returned object forms the public API. ES6 modules achieve the same goal more cleanly — variables declared in a module are private by default and only exported names are public. The module pattern is still relevant for understanding encapsulation and is used in third-party scripts that must not pollute the global scope.

43. What is the Observer pattern (Pub/Sub)?

The Observer pattern defines a one-to-many dependency where multiple subscribers are notified when a subject's state changes. In JavaScript: class EventEmitter { constructor() { this.events = {}; } on(event, listener) { (this.events[event] ||= []).push(listener); } emit(event, ...args) { this.events[event]?.forEach(l => l(...args)); } }. Node.js's built-in EventEmitter class follows this pattern. The DOM's addEventListener/removeEventListener is a native implementation. React state management libraries (Redux, Zustand) use the observer pattern to notify components when store state changes.

44. What is tree shaking?

Tree shaking is a dead code elimination technique used by module bundlers (Webpack, Rollup, Vite) that removes unused code (exports) from the final bundle, reducing its size. It works with ES6 import/export (static module syntax) because the bundler can statically analyse which exports are actually used. CommonJS require() is dynamic and cannot be tree-shaken. For a library to be tree-shakeable: use named exports (not a single default export object), use ES modules, and avoid side effects at the module level (mark side-effect-free packages with "sideEffects": false in package.json).

45. What is lazy loading?

Lazy loading defers the loading of non-critical resources (images, scripts, components) until they are needed, improving initial page load performance. For images: <img loading="lazy" src="..."> defers loading until the image is near the viewport. For JavaScript: dynamic import() loads modules on demand: button.addEventListener('click', async () => { const { default: Chart } = await import('./chart.js'); new Chart(data); }). In React, React.lazy() with Suspense lazy loads components. Code splitting (separating a bundle into chunks loaded on demand) is the foundation of lazy loading in SPAs.

46. What is the Proxy object?

Proxy wraps an object and intercepts fundamental operations (property access, assignment, function calls) via handler traps. new Proxy(target, handler) where handler defines traps like get, set, deleteProperty, has, and apply. Example: new Proxy({}, { get(target, key) { return key in target ? target[key] : 'default'; } }). Proxies enable: data validation on assignment, computed properties, logging, reactive systems (Vue 3's reactivity system uses Proxies), and creating APIs that appear to have infinite properties. Reflect methods are the default implementations for each trap.

47. What is TypeScript and why is it used with JavaScript?

TypeScript is a statically-typed superset of JavaScript developed by Microsoft. It adds optional type annotations, interfaces, enums, generics, and compile-time type checking, catching type errors before runtime. TypeScript compiles to plain JavaScript. Benefits: better IDE support (autocompletion, inline errors), self-documenting code, safer refactoring, and catching common bugs (calling a method on undefined, passing wrong argument types). It is the standard in enterprise JavaScript development. Frameworks like Angular are written in TypeScript; React and Vue have strong TypeScript support. TypeScript adoption has grown dramatically since 2018.

48. What is the difference between for...in and for...of?

for...in iterates over all enumerable property keys of an object (including inherited ones from the prototype chain). It is designed for objects: for (const key in obj) { ... }. It should not be used on arrays because it may include prototype properties and does not guarantee order. for...of iterates over the values of any iterable (arrays, strings, Maps, Sets, generators) using the Symbol.iterator protocol. It does not work on plain objects (unless they implement Symbol.iterator). Use for...in for object keys; use for...of for iterables; use Object.keys(), Object.values(), or Object.entries() with for...of for objects.

49. What are the key array methods in JavaScript?

Transformation: map() creates a new array by applying a function to each element; filter() creates a new array with elements passing a test; reduce() accumulates a single value from the array. Search: find() returns the first matching element; findIndex() returns the index; some() tests if any element passes; every() tests if all elements pass; includes() checks membership. Mutation: push()/pop() add/remove from end; unshift()/shift() add/remove from front; splice() removes/inserts at index; sort() sorts in place. Flattening: flat(), flatMap(). All transform methods return new arrays; mutation methods modify in place.

50. What are common JavaScript performance optimisation techniques?

Key techniques: minimise DOM manipulation (batch updates, use DocumentFragment, avoid layout thrashing by not reading and writing layout properties in the same frame); use requestAnimationFrame for animations; debounce/throttle event handlers; lazy load resources; use Web Workers for CPU-intensive tasks (off the main thread); cache DOM queries (const el = document.querySelector(...) once, not in a loop); use const and let instead of var for better engine optimisation; avoid memory leaks (remove event listeners, clear timers, use WeakMap/WeakRef for caches); use service workers for caching and offline support; measure with Lighthouse and Chrome DevTools Performance panel.