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. How to read and draw the class and sequence diagrams that describe patterns is in ERD & UML diagrams.
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.
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
newis 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.createreturnsany, so annotate;structuredClonedrops 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.
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);
#privatefields 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| Trap | Intercepts | Reflect twin |
|---|---|---|
get / set | property read / write | Reflect.get / .set |
has | key in obj | Reflect.has |
deleteProperty | delete obj.key | Reflect.deleteProperty |
ownKeys | Object.keys, for...in | Reflect.ownKeys |
apply | calling a function proxy | Reflect.apply |
construct | new on a function proxy | Reflect.construct |
- Use when: validation, logging, reactivity (Vue, MobX), lazy or remote objects.
- Watch out: slower than plain access; breaks on objects with
#privatefields and on built-ins with internal slots (Map,Date);setmust returntrueor 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);experimentalDecoratorsis 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
WeakRefor 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
typealias (interfaces have no implicit index signature, so they failRecord<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
switchover "how" keeps growing; sorting comparators, auth providers. - Watch out:
satisfieskeeps the literal keys; aRecord<string, Pricing>annotation would widenTiertostring.
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 withuse(Ctx).
Rendering patterns
Where and when HTML gets produced. Most frameworks (Next.js, Remix/React Router, Astro, Nuxt) let you mix these per route.
| Pattern | Renders where / when | Pros | Cons |
|---|---|---|---|
| CSR (client-side) | browser, after JS downloads; server sends an empty shell | cheap hosting, rich interactivity, simple mental model | slow first paint and LCP on weak devices, SEO relies on crawler JS, big bundles |
| SSR (server-side) | server, on every request; client hydrates | fresh per-request data, good SEO, fast FCP | server cost, TTFB grows with slowest query, hydration cost (TTI) |
| SSG (static) | build time; served from CDN | fastest TTFB, cheap, cacheable | stale until rebuild; builds slow with many pages |
| ISR (incremental static) | build time, then regenerated in the background after a TTL or on-demand revalidation | CDN speed with periodic freshness | briefly stale content; platform support needed |
| Streaming SSR | server, sends HTML in chunks as Suspense boundaries resolve | early first byte, slow parts don't block fast ones | needs streaming-capable host; layout shift if fallbacks are poor |
| React Server Components | server (at build or request); only client components ship JS | zero JS for server parts, direct DB access, smaller bundles | server/client boundary rules, framework-bound, serializable props only |
| Islands | static 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).
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.
| Pattern | Idea | How |
|---|---|---|
| Bundle splitting | many small chunks instead of one bundle; better caching | bundler splitChunks / automatic in Vite, Next |
| Dynamic import | load a module only when needed | await import("./x.js") |
| Import on interaction | fetch code on click/hover/focus | dynamic import in the event handler |
| Import on visibility | fetch when scrolled near | IntersectionObserver + dynamic import |
| Route-based splitting | one chunk per route | React.lazy, framework routers do it by default |
| Preload | high-priority fetch of a resource needed now | <link rel="preload">, modulepreload |
| Prefetch | low-priority fetch of a likely next resource | <link rel="prefetch">, <Link prefetch> |
| Tree shaking | drop unused exports at build time | ESM, named exports, "sideEffects": false |
| PRPL | Push critical, Render initial route, Pre-cache rest, Lazy-load | preload + SSR + service worker + splitting |
| List virtualization | render only visible rows | TanStack Virtual, react-window, content-visibility |
| Compression | shrink text assets on the wire | Brotli / 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.tsre-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
| Encoding | Ratio | Notes |
|---|---|---|
| gzip | good | universal fallback |
Brotli (br) | 15-25% smaller than gzip on text | all modern browsers over HTTPS; precompress at level 11 |
| zstd | close to Brotli, faster to compress | newer; check browser support before relying on it |
Never compress already-compressed formats (JPEG, PNG, WOFF2, video).
Choosing a pattern
| Problem | Reach for |
|---|---|
| exactly one shared instance | ES module (singleton) |
| creation varies by input | factory |
| many optional construction steps | builder, or options object |
| validate or observe property access | proxy |
| add retry/logging/caching to a function | decorator (wrapper) |
| third-party API doesn't fit your interface | adapter |
| callers repeat the same setup | facade |
| thousands of near-identical objects | flyweight |
| notify many listeners of changes | observer / event emitter |
| request pipeline, cross-cutting steps | middleware |
| undo/redo, queues, replay | command |
| swap algorithms at runtime | strategy (function map) |
| lazy or infinite sequences | generators |
| mutually exclusive UI states | state machine (discriminated union) |
| share stateful logic in React | custom hook |
| flexible multi-part component | compound components |
| app-wide value in React | provider (context) |
| content-heavy, SEO pages | SSG / ISR / RSC |
| heavy code used rarely | dynamic import on interaction/visibility |
| huge lists | virtualization |
References
- MDN: Proxy (opens in a new tab), Reflect (opens in a new tab), Iterator helpers (opens in a new tab), rel=preload (opens in a new tab)
- patterns.dev (opens in a new tab): design, rendering and performance patterns
- TypeScript handbook: Mixins (opens in a new tab), Decorators (5.0 notes) (opens in a new tab)
- React: Reusing logic with custom hooks (opens in a new tab), Passing data deeply with context (opens in a new tab)
- web.dev: Rendering on the web (opens in a new tab)