../

Design patterns

Classic design patterns, React component patterns, rendering strategies and loading-performance patterns, following the three sections of patterns.dev, written in TypeScript. Each entry gives the intent, a short typed example, when to reach for it and what to watch out for.

Creational

Patterns that control how objects get made.

Singleton

One shared instance for the whole program. In JavaScript an ES module is already a singleton: its body runs once and every importer gets the same bindings.

config.ts
type Settings = { theme: "light" | "dark"; debug: boolean };
 
let settings: Settings = { theme: "dark", debug: false };
 
export const config = {
  get: (): Readonly<Settings> => settings,
  set: (next: Partial<Settings>): void => {
    settings = { ...settings, ...next };
  },
};

The class form, when you need lazy creation or instanceof:

class Counter {
  static #instance: Counter | undefined;
  #count = 0;
  private constructor() {}
 
  static get instance(): Counter {
    return (Counter.#instance ??= new Counter());
  }
  increment(): number {
    return ++this.#count;
  }
}
 
Counter.instance.increment(); // 1
// @ts-expect-error constructor is private
new Counter();
  • Use when: one logger, one DB pool, one config object.
  • Watch out: hidden global state, order-dependent tests, and two copies when a package is installed twice or loaded as both CJS and ESM (the dual-package hazard in Modules & packages). Prefer passing the instance in (dependency injection) where tests need to swap it.

Factory

A function that decides which concrete object to build and hides new.

type Shape =
  | { kind: "circle"; r: number }
  | { kind: "rect"; w: number; h: number };
 
interface Area {
  area(): number;
}
 
function createShape(s: Shape): Area {
  switch (s.kind) {
    case "circle":
      return { area: () => Math.PI * s.r ** 2 };
    case "rect":
      return { area: () => s.w * s.h };
  }
}
 
createShape({ kind: "rect", w: 2, h: 3 }).area(); // 6
  • Use when: creation depends on config or input, or you want to return plain objects instead of class instances.
  • Watch out: a factory that only wraps one new is noise.

Builder

Assemble a complex object step by step through a fluent API. This version is immutable: every step returns a new builder, so partial builders can be reused.

interface Query {
  table: string;
  where: readonly string[];
  limit?: number;
}
 
interface QueryBuilder {
  where(cond: string): QueryBuilder;
  limit(n: number): QueryBuilder;
  toSQL(): string;
}
 
function build(q: Query): QueryBuilder {
  return {
    where: (c) => build({ ...q, where: [...q.where, c] }),
    limit: (n) => build({ ...q, limit: n }),
    toSQL: () =>
      [
        `SELECT * FROM ${q.table}`,
        q.where.length
          ? `WHERE ${q.where.join(" AND ")}`
          : "",
        q.limit !== undefined ? `LIMIT ${q.limit}` : "",
      ]
        .filter(Boolean)
        .join(" "),
  };
}
 
const from = (table: string) => build({ table, where: [] });
const active = from("users").where("active = true");
active.limit(10).toSQL();
// SELECT * FROM users WHERE active = true LIMIT 10
  • Use when: many optional parts, or construction order matters.
  • Watch out: an options object with defaults ({ ...defaults, ...opts }) is often enough.

Prototype

New objects delegate to an existing object instead of copying behavior. Every JS class is sugar over this: methods live once on Class.prototype.

const proto = {
  describe(this: { name: string }): string {
    return `I am ${this.name}`;
  },
};
type Named = typeof proto & { name: string };
 
const rex: Named = Object.create(proto);
rex.name = "Rex"; // own property; describe() is shared
rex.describe(); // "I am Rex"
Object.getPrototypeOf(rex) === proto; // true
 
// Copying data rather than sharing behavior: deep clone
const copy = structuredClone({
  tags: ["a"],
  at: new Date(),
});
  • Use when: many objects share methods (memory) or you clone templates.
  • Watch out: Object.create returns any, so annotate; structuredClone drops functions and class prototypes.

Structural

Patterns for composing objects and modules.

Module

Encapsulate private state behind an explicit public API. ES modules do this natively: anything not exported is private.

cart.ts
const items = new Map<string, number>(); // private
 
export function add(sku: string, qty = 1): void {
  items.set(sku, (items.get(sku) ?? 0) + qty);
}
export const count = (): number =>
  [...items.values()].reduce((a, b) => a + b, 0);
  • Use when: always; it is the default unit of code in TS.
  • Watch out: module state is a singleton (see above); #private fields do the same job inside classes.

Proxy

Intercept operations on an object (get, set, has, delete, apply) with Proxy, and forward the default behavior with Reflect.

const user = { name: "Ada", age: 36 };
 
const guarded = new Proxy(user, {
  get(target, key, receiver) {
    if (!Reflect.has(target, key)) {
      throw new Error(`No property ${String(key)}`);
    }
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value: unknown, receiver) {
    if (key === "age" && typeof value !== "number") {
      throw new TypeError("age must be a number");
    }
    return Reflect.set(target, key, value, receiver);
  },
});
 
guarded.age = 37; // ok
TrapInterceptsReflect twin
get / setproperty read / writeReflect.get / .set
haskey in objReflect.has
deletePropertydelete obj.keyReflect.deleteProperty
ownKeysObject.keys, for...inReflect.ownKeys
applycalling a function proxyReflect.apply
constructnew on a function proxyReflect.construct
  • Use when: validation, logging, reactivity (Vue, MobX), lazy or remote objects.
  • Watch out: slower than plain access; breaks on objects with #private fields and on built-ins with internal slots (Map, Date); set must return true or strict mode throws.

Decorator

Add behavior to a function without changing it by wrapping it. Higher-order functions keep the signature through generics.

function withRetry<A extends unknown[], R>(
  fn: (...args: A) => Promise<R>,
  attempts = 3,
): (...args: A) => Promise<R> {
  return async (...args) => {
    let lastError: unknown;
    for (let i = 0; i < attempts; i++) {
      try {
        return await fn(...args);
      } catch (err) {
        lastError = err;
      }
    }
    throw lastError;
  };
}
 
const getUser = async (id: string) => ({ id });
// (id: string) => ...
const robustGetUser = withRetry(getUser);

TS 5.0+ also implements standard @decorator syntax for classes and members:

function logged<This, A extends unknown[], R>(
  target: (this: This, ...args: A) => R,
  ctx: ClassMethodDecoratorContext<This>,
) {
  return function (this: This, ...args: A): R {
    console.debug(`-> ${String(ctx.name)}`);
    return target.call(this, ...args);
  };
}
 
class Api {
  @logged
  getUser(id: string) {
    return { id };
  }
}
  • Use when: cross-cutting concerns: retry, timing, caching, auth checks.
  • Watch out: @ decorators need compiling (not erasable, not in browsers or Node type stripping yet); experimentalDecorators is the older, incompatible flavor.

Adapter

Wrap an incompatible interface so it fits the one your code expects.

interface Logger {
  log(level: "info" | "error", msg: string): void;
}
 
class LegacyLogger {
  write(line: string): void {
    process.stdout.write(line + "\n");
  }
}
 
const adapt = (legacy: LegacyLogger): Logger => ({
  log: (level, msg) => legacy.write(`[${level}] ${msg}`),
});
  • Use when: integrating third-party SDKs, swapping vendors, testing with fakes.
  • Watch out: adapters that leak the wrapped type defeat the point.

Facade

One simple function in front of a fiddly subsystem.

export async function getJSON<T>(
  url: string,
  parse: (data: unknown) => T,
): Promise<T> {
  const res = await fetch(url, {
    headers: { Accept: "application/json" },
    signal: AbortSignal.timeout(5_000),
  });
  if (!res.ok) throw new Error(`${res.status} ${url}`);
  return parse(await res.json());
}
  • Use when: callers repeat the same multi-step setup (see Fetch API).
  • Watch out: facades grow into god objects; keep the escape hatch to the full API.

Flyweight

Share immutable intrinsic state between many objects; keep per-object (extrinsic) state outside.

interface Glyph {
  readonly char: string;
  readonly font: string;
}
const glyphs = new Map<string, Glyph>();
 
function glyph(char: string, font: string): Glyph {
  const key = `${char}:${font}`;
  const hit = glyphs.get(key);
  if (hit) return hit;
  const g = Object.freeze({ char, font });
  glyphs.set(key, g);
  return g;
}
 
// position (extrinsic) lives outside the shared glyph
const line = [..."hello"].map((c, x) => ({
  g: glyph(c, "mono"),
  x,
}));
  • Use when: very many similar objects (tiles, glyphs, map markers).
  • Watch out: the cache itself can leak; consider WeakRef or an LRU bound.

Mixin

Compose behavior into classes without deep inheritance. A mixin is a function from a class to a subclass.

type Ctor<T = object> = new (...args: any[]) => T;
 
function Timestamped<B extends Ctor>(Base: B) {
  return class extends Base {
    createdAt = new Date();
  };
}
function Taggable<B extends Ctor>(Base: B) {
  return class extends Base {
    tags = new Set<string>();
  };
}
 
class User {
  name = "anon";
}
const RichUser = Taggable(Timestamped(User));
const u = new RichUser();
u.createdAt; u.tags; u.name; // all typed
  • Use when: a few orthogonal capabilities shared across unrelated classes.
  • Watch out: name clashes and opaque types; plain composition (a field holding a helper) or functions over data is usually clearer. React dropped mixins for hooks.

Behavioral

Patterns for how objects communicate.

Observer

Subjects notify subscribers when something happens. A typed event emitter maps event names to payload types.

type Listener<T> = (payload: T) => void;
 
class Emitter<E extends Record<string, unknown>> {
  #subs: { [K in keyof E]?: Set<Listener<E[K]>> } = {};
 
  on<K extends keyof E>(type: K, fn: Listener<E[K]>) {
    const set = (this.#subs[type] ??= new Set());
    set.add(fn);
    return () => void set.delete(fn); // unsubscribe
  }
 
  emit<K extends keyof E>(type: K, payload: E[K]): void {
    this.#subs[type]?.forEach((fn) => fn(payload));
  }
}
 
type AppEvents = {
  login: { id: string };
  logout: undefined;
};
const bus = new Emitter<AppEvents>();
 
const off = bus.on("login", (e) => e.id.toUpperCase());
bus.emit("login", { id: "u1" });
// @ts-expect-error id must be a string
bus.emit("login", { id: 1 });
off();

Built-in alternatives: EventTarget + CustomEvent (browser and Node), Node's EventEmitter<{ login: [id: string] }> (generic in @types/node), and signals/observables.

  • Use when: decoupling producers from consumers, UI state, domain events.
  • Watch out: forgotten unsubscribes leak memory; the event map must be a type alias (interfaces have no implicit index signature, so they fail Record<string, unknown>).

Mediator and middleware

A mediator centralizes communication so components talk to it, not to each other. Middleware is a mediator shaped as a pipeline: each step can act, then call next().

type Ctx = { path: string; user?: string; status?: number };
type Next = () => Promise<void>;
type Middleware = (ctx: Ctx, next: Next) => Promise<void>;
 
function compose(stack: readonly Middleware[]) {
  return (ctx: Ctx): Promise<void> => {
    const run = (i: number): Promise<void> => {
      const mw = stack[i];
      return mw
        ? mw(ctx, () => run(i + 1))
        : Promise.resolve();
    };
    return run(0);
  };
}
 
const auth: Middleware = async (ctx, next) => {
  if (!ctx.user) {
    ctx.status = 401;
    return;
  }
  await next();
};
const handler = compose([auth, async (ctx) => {
  ctx.status = 200;
}]);
  • Use when: request pipelines (Express, Koa, Hono), chat rooms, form field coordination, Redux middleware.
  • Watch out: the mediator becomes the one class that knows everything; order of middleware is behavior.

Command

Turn an action into a value so it can be queued, logged, retried or undone.

type State = { items: readonly string[] };
 
interface Command {
  apply(s: State): State;
  revert(s: State): State;
}
 
const addItem = (item: string): Command => ({
  apply: (s) => ({ items: [...s.items, item] }),
  revert: (s) => ({ items: s.items.slice(0, -1) }),
});
 
let state: State = { items: [] };
let done: readonly Command[] = [];
 
function exec(cmd: Command): void {
  state = cmd.apply(state);
  done = [...done, cmd];
}
function undo(): void {
  const last = done.at(-1);
  if (!last) return;
  state = last.revert(state);
  done = done.slice(0, -1);
}
  • Use when: undo/redo, job queues, macro recording, Redux actions (commands as data).
  • Watch out: commands that capture stale state; keep them pure where possible.

Strategy

Pick an interchangeable algorithm at runtime. In TS a strategy is usually just a function in a lookup table.

type Pricing = (subtotal: number) => number;
 
const pricing = {
  regular: (n) => n,
  member: (n) => n * 0.9,
  clearance: (n) => Math.max(0, n - 20),
} satisfies Record<string, Pricing>;
 
type Tier = keyof typeof pricing; // "regular" | ...
const total = (n: number, tier: Tier) => pricing[tier](n);
  • Use when: a switch over "how" keeps growing; sorting comparators, auth providers.
  • Watch out: satisfies keeps the literal keys; a Record<string, Pricing> annotation would widen Tier to string.

Iterator and generators

Walk a sequence without exposing its structure. Generators make any object iterable and lazy.

function* range(start: number, end: number, step = 1) {
  for (let i = start; i < end; i += step) yield i;
}
[...range(0, 5)]; // [0, 1, 2, 3, 4]
 
class Bag<T> implements Iterable<T> {
  #items: readonly T[];
  constructor(items: readonly T[]) {
    this.#items = items;
  }
  *[Symbol.iterator]() {
    yield* this.#items; // delegate to the array
  }
}
for (const x of new Bag([1, 2])) x;
 
// ES2025 iterator helpers: lazy map/filter/take
range(0, Infinity)
  .filter((n) => n % 7 === 0)
  .take(3)
  .toArray();

Async generators page through remote data:

type Page<T> = { items: T[]; next: string | null };
 
async function* paginate<T>(first: string) {
  let url: string | null = first;
  while (url) {
    const res: Response = await fetch(url);
    const page = (await res.json()) as Page<T>;
    yield* page.items;
    url = page.next;
  }
}
// for await (const user of paginate<User>("/api/users")) ...
  • Use when: lazy or infinite sequences, streaming, custom collections.
  • Watch out: iterator helpers are Baseline 2025 (older Safari lacks them); generators are single-use.

State machine

Model states and transitions explicitly with discriminated unions so impossible states can't be represented. See Fundamentals for narrowing.

type State =
  | { status: "idle" }
  | { status: "loading"; startedAt: number }
  | { status: "success"; data: string }
  | { status: "error"; error: Error };
 
type Msg =
  | { type: "FETCH" }
  | { type: "RESOLVE"; data: string }
  | { type: "REJECT"; error: Error }
  | { type: "RESET" };
 
const assertNever = (x: never): never => {
  throw new Error(`Unhandled: ${JSON.stringify(x)}`);
};
 
function transition(s: State, m: Msg): State {
  switch (s.status) {
    case "idle":
      return m.type === "FETCH"
        ? { status: "loading", startedAt: Date.now() }
        : s;
    case "loading":
      if (m.type === "RESOLVE") {
        return { status: "success", data: m.data };
      }
      if (m.type === "REJECT") {
        return { status: "error", error: m.error };
      }
      return s;
    case "success":
    case "error":
      return m.type === "RESET" ? { status: "idle" } : s;
    default:
      return assertNever(s);
  }
}
  • Use when: UI flows (fetch, wizard, checkout), protocols, anything with "is it loading and errored at once?" bugs. Plugs straight into useReducer.
  • Watch out: big machines get hard to read; XState adds visualization, guards, actors.

React component patterns

React 19 with TypeScript; see TypeScript + React for prop typing. Hooks have replaced most HOC and render-prop uses.

Container / presentational

Split data fetching (container) from rendering (presentational). With Server Components the container is an async server component.

type User = { id: string; name: string };
declare function getUsers(): Promise<User[]>;
 
// presentational: pure props in, JSX out
function UserList({ users }: { users: User[] }) {
  return (
    <ul>
      {users.map((u) => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}
 
// container: owns the data
export async function UsersPage() {
  const users = await getUsers();
  return <UserList users={users} />;
}
  • Use when: you want presentational components that are easy to test and reuse.
  • Watch out: in client-only apps a custom hook (useUsers()) usually replaces the container.

Higher-order components

A function that takes a component and returns an enhanced one.

import type { ComponentType } from "react";
 
function withLoading<P extends object>(
  Wrapped: ComponentType<P>,
) {
  return function WithLoading(
    props: P & { loading: boolean },
  ) {
    const { loading, ...rest } = props;
    if (loading) return <p>Loading…</p>;
    return <Wrapped {...(rest as unknown as P)} />;
  };
}
  • Use when: wrapping third-party components, or cross-cutting concerns in libraries (memo, forwardRef-era APIs are HOCs).
  • Watch out: prop-name collisions, "wrapper hell" in DevTools, awkward generics (the cast above). Prefer a hook.

Render props

Pass a function that returns UI, so the component shares state without owning the markup.

import type { ReactNode } from "react";
 
type Pos = { x: number; y: number };
 
type MouseProps = { children: (p: Pos) => ReactNode };
 
// useMousePosition: see Hooks below
function Mouse({ children }: MouseProps) {
  return children(useMousePosition());
}
 
const View = () => (
  <Mouse>{({ x, y }) => <p>{x}, {y}</p>}</Mouse>
);
  • Use when: the consumer must control rendering (virtualized lists renderItem, headless UI libraries).
  • Watch out: nesting several render props gets deep; a hook composes flatter.

Hooks

Reusable stateful logic as a function whose name starts with use. The same mouse logic:

import { useEffect, useState } from "react";
 
export function useMousePosition() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const move = (e: MouseEvent) =>
      setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener("mousemove", move);
    return () =>
      window.removeEventListener("mousemove", move);
  }, []);
  return pos;
}
 
function Cursor() {
  const { x, y } = useMousePosition();
  return <p>{x}, {y}</p>;
}
  • Use when: by default for sharing logic between components.
  • Watch out: rules of hooks (top level, same order every render); a hook shares logic, not state (each caller gets its own).

Compound components

A parent shares implicit state with its named children through context, giving a flexible, declarative API (Tabs.Tab, Tabs.Panel).

import { createContext, use, useState } from "react";
import type { ReactNode } from "react";
 
type TabsState = {
  active: string;
  select: (id: string) => void;
};
const TabsCtx = createContext<TabsState | null>(null);
 
function useTabs(): TabsState {
  const ctx = use(TabsCtx);
  if (!ctx) throw new Error("Tabs.* must be inside <Tabs>");
  return ctx;
}
 
type Kids = { children: ReactNode };
 
type TabsProps = Kids & { initial: string };
 
export function Tabs({ initial, children }: TabsProps) {
  const [active, select] = useState(initial);
  return (
    <TabsCtx value={{ active, select }}>{children}</TabsCtx>
  );
}
 
function Tab({ id, children }: Kids & { id: string }) {
  const { active, select } = useTabs();
  return (
    <button
      aria-selected={active === id}
      onClick={() => select(id)}
    >
      {children}
    </button>
  );
}
 
function Panel({ id, children }: Kids & { id: string }) {
  return useTabs().active === id ? (
    <div>{children}</div>
  ) : null;
}
 
Tabs.Tab = Tab;
Tabs.Panel = Panel;
  • Use when: component libraries: selects, menus, accordions, tabs.
  • Watch out: children must render inside the parent's tree; give a clear error when not.

Provider

Make a value available to a whole subtree through context instead of prop drilling.

import { createContext, use, useState } from "react";
import type { ReactNode } from "react";
 
type Kids = { children: ReactNode };
type Theme = "light" | "dark";
type ThemeCtx = { theme: Theme; toggle: () => void };
 
const ThemeContext = createContext<ThemeCtx | null>(null);
 
export function ThemeProvider({ children }: Kids) {
  const [theme, setTheme] = useState<Theme>("light");
  const toggle = () =>
    setTheme((t) => (t === "light" ? "dark" : "light"));
  return (
    <ThemeContext value={{ theme, toggle }}>
      {children}
    </ThemeContext>
  );
}
 
export function useTheme(): ThemeCtx {
  const ctx = use(ThemeContext);
  if (!ctx) {
    throw new Error("useTheme needs <ThemeProvider>");
  }
  return ctx;
}
  • Use when: theme, auth user, locale, feature flags: values read in many places.
  • Watch out: every consumer re-renders when the value changes; split fast-changing values into their own context or use a store (Zustand, Jotai). React 19 renders <Ctx> directly as the provider and reads it with use(Ctx).

Rendering patterns

Where and when HTML gets produced. Most frameworks (Next.js, Remix/React Router, Astro, Nuxt) let you mix these per route.

PatternRenders where / whenProsCons
CSR (client-side)browser, after JS downloads; server sends an empty shellcheap hosting, rich interactivity, simple mental modelslow first paint and LCP on weak devices, SEO relies on crawler JS, big bundles
SSR (server-side)server, on every request; client hydratesfresh per-request data, good SEO, fast FCPserver cost, TTFB grows with slowest query, hydration cost (TTI)
SSG (static)build time; served from CDNfastest TTFB, cheap, cacheablestale until rebuild; builds slow with many pages
ISR (incremental static)build time, then regenerated in the background after a TTL or on-demand revalidationCDN speed with periodic freshnessbriefly stale content; platform support needed
Streaming SSRserver, sends HTML in chunks as Suspense boundaries resolveearly first byte, slow parts don't block fast onesneeds streaming-capable host; layout shift if fallbacks are poor
React Server Componentsserver (at build or request); only client components ship JSzero JS for server parts, direct DB access, smaller bundlesserver/client boundary rules, framework-bound, serializable props only
Islandsstatic HTML with isolated interactive widgets hydrated independently (Astro, Fresh)minimal JS, per-island loading (client:visible)cross-island state is awkward; less suited to app-like UIs

Related hydration techniques: progressive hydration (hydrate parts later or on visibility), selective hydration (React 18+ hydrates Suspense boundaries independently and prioritizes the ones the user interacts with), and partial prerendering (static shell plus streamed dynamic holes).

app/page.tsx
import { Suspense, type ReactNode } from "react";
 
declare function Recommendations(): Promise<ReactNode>;
 
export default function Page() {
  return (
    <main>
      <h1>Shop</h1> {/* in the first flush */}
      <Suspense fallback={<p>Loading picks…</p>}>
        <Recommendations /> {/* streamed in later */}
      </Suspense>
    </main>
  );
}

Performance patterns

Load less, load later, load in the right order.

PatternIdeaHow
Bundle splittingmany small chunks instead of one bundle; better cachingbundler splitChunks / automatic in Vite, Next
Dynamic importload a module only when neededawait import("./x.js")
Import on interactionfetch code on click/hover/focusdynamic import in the event handler
Import on visibilityfetch when scrolled nearIntersectionObserver + dynamic import
Route-based splittingone chunk per routeReact.lazy, framework routers do it by default
Preloadhigh-priority fetch of a resource needed now<link rel="preload">, modulepreload
Prefetchlow-priority fetch of a likely next resource<link rel="prefetch">, <Link prefetch>
Tree shakingdrop unused exports at build timeESM, named exports, "sideEffects": false
PRPLPush critical, Render initial route, Pre-cache rest, Lazy-loadpreload + SSR + service worker + splitting
List virtualizationrender only visible rowsTanStack Virtual, react-window, content-visibility
Compressionshrink text assets on the wireBrotli / gzip / zstd, precompressed at build

Dynamic import and import on interaction

const button = document.querySelector("#export")!;
 
button.addEventListener("click", async () => {
  // chunk is fetched on the first click, then cached
  const { toCSV } = await import("./csv-export.js");
  toCSV([{ a: 1 }]);
});

Import on visibility

function onVisible(
  el: Element,
  load: () => Promise<unknown>,
) {
  const io = new IntersectionObserver((entries) => {
    if (entries.some((e) => e.isIntersecting)) {
      io.disconnect();
      void load();
    }
  }, { rootMargin: "200px" }); // start a bit early
  io.observe(el);
}
 
onVisible(document.querySelector("#chart")!, () =>
  import("./chart.js").then((m) => m.render("#chart")),
);

Route-based splitting

import { lazy, Suspense } from "react";
 
// default export required by lazy()
const Settings = lazy(() => import("./Settings.js"));
 
export function App({ route }: { route: string }) {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      {route === "/settings" ? <Settings /> : <p>Home</p>}
    </Suspense>
  );
}

Preload and prefetch

<!-- needed for this page, discovered late
     (font, hero, LCP) -->
<link rel="preload" href="/fonts/inter.woff2" as="font"
      type="font/woff2" crossorigin>
<link rel="modulepreload" href="/assets/app.js">
<!-- probably needed next: idle-time, low priority -->
<link rel="prefetch" href="/assets/checkout.js">
<link rel="preconnect" href="https://api.example.com">

Tree shaking

  • Ship ESM with named exports; CommonJS is hard to shake.
  • Mark the package side-effect free: "sideEffects": false (or list CSS files that are).
  • Avoid barrel files (index.ts re-exporting everything) in apps; they defeat splitting in some bundlers and slow dev servers.
  • /* @__PURE__ */ marks a call as droppable if its result is unused.

List virtualization

The core arithmetic every virtualizer does:

function visibleRange(
  scrollTop: number,
  viewport: number,
  rowHeight: number,
  total: number,
  overscan = 5,
): { start: number; end: number; offset: number } {
  const first = Math.floor(scrollTop / rowHeight);
  const count = Math.ceil(viewport / rowHeight);
  const start = Math.max(0, first - overscan);
  const end = Math.min(total, first + count + overscan);
  return { start, end, offset: start * rowHeight };
}

Render rows start to end inside a spacer of height total * rowHeight, translated by offset. For plain long pages, CSS content-visibility: auto skips rendering off-screen sections with no JS.

Compression

EncodingRatioNotes
gzipgooduniversal fallback
Brotli (br)15-25% smaller than gzip on textall modern browsers over HTTPS; precompress at level 11
zstdclose to Brotli, faster to compressnewer; check browser support before relying on it

Never compress already-compressed formats (JPEG, PNG, WOFF2, video).

Choosing a pattern

ProblemReach for
exactly one shared instanceES module (singleton)
creation varies by inputfactory
many optional construction stepsbuilder, or options object
validate or observe property accessproxy
add retry/logging/caching to a functiondecorator (wrapper)
third-party API doesn't fit your interfaceadapter
callers repeat the same setupfacade
thousands of near-identical objectsflyweight
notify many listeners of changesobserver / event emitter
request pipeline, cross-cutting stepsmiddleware
undo/redo, queues, replaycommand
swap algorithms at runtimestrategy (function map)
lazy or infinite sequencesgenerators
mutually exclusive UI statesstate machine (discriminated union)
share stateful logic in Reactcustom hook
flexible multi-part componentcompound components
app-wide value in Reactprovider (context)
content-heavy, SEO pagesSSG / ISR / RSC
heavy code used rarelydynamic import on interaction/visibility
huge listsvirtualization

References