#performance
24 posts
Putting a Three.js island inside a static site
The isle page downloads only its own script. The scene is computed at runtime, Three.js stays in that page's chunk, and the first HTML response carries an empty stage.
Twelve hundred instances: squeezing a garden into a few draw calls
Six hundred blades of grass, a hundred and twenty flowers, eighty-five mossy stones, forty-two shrubs. Density comes from InstancedMesh, not from cutting the count.
The loading budget for 3D: split the chunk, load on visibility, know when to stop
A 3D page pays three costs — download, first-frame compilation, per-frame drawing. Each needs its own budget, and any one of them can turn good-looking into janky.
Misusing big O: constants and preconditions decide more than the order
An O(n log n) sort can lose to O(n²) insertion sort, and an O(1) hash lookup can lose to an array scan. Check input size, constants and memory locality before judging speed.
Debounce vs throttle: the difference is who decides when to run
Both limit how often a function runs. Debounce lets the last call decide the moment; throttle uses a fixed window. The wrong pick shows up as a laggy drag or a request every 200ms.
The game loop: why delta time is not optional
Moving 5 pixels per frame runs twice as fast on a 60Hz screen and two and a half times as fast at 144Hz. Multiply by elapsed time to decouple physics from refresh rate, then cap it.
HTTP caching: Cache-Control and ETag each own half
Cache-Control decides whether a request happens; ETag decides whether the response body can be skipped. Mixing them is why cache bugs persist: changing one never fixes the other half.
Memory layout: the same algorithm, ten times slower, is usually cache
A trip to main memory costs hundreds of cycles; a cache line fetch costs one. Sequential versus random access often outweighs the algorithmic order itself.
The N+1 query: the most typical ORM performance trap
Fetch 100 rows and query the relation for each one and you have made 101 queries. There are only three fixes: eager load, batch fetch, or use a single join.
Rate limiting: what token bucket and leaky bucket really differ on
A token bucket permits bursts; a leaky bucket flattens traffic to a constant rate. Picking wrong shows up as throttled normal users, or a limit that does nothing.
Sliding windows: decide what the window tracks before writing the loop
A window works when validity changes monotonically with its edges. Write down the tracked state and the shrink condition first, and the two-pointer loop amortises to O(n) on its own.
Monotonic stacks: next greater element in O(n), not O(n squared)
Each index is pushed and popped at most once, so what looks like a nested loop is linear. Recognise the first larger or smaller neighbour shape and the same template applies.
Counting the ones in a binary number: from loops to SWAR
Popcount has three forms: loop once per set bit, a lookup table, or SWAR across bit fields. They differ by orders of magnitude, and Rust and WebAssembly already expose it as a single instruction.
Union-find: two lines of optimisation make every operation near constant
Path compression flattens the tree, union by size stops it growing tall. You need both before the cost drops to the inverse Ackermann function, which is never above 5 in practice.
Islands are not Web Components: two different kinds of on-demand loading
Islands decide at build time where JavaScript is needed and when it hydrates. Web Components give the browser a self-contained element at runtime. Confusing them picks the wrong tool.
Cache headers for a static site: hashed files forever, HTML never
A cache policy depends on whether the filename changes with the content, not on file size. Long-caching HTML hands users an old entry point that points at deleted chunks.
When WebAssembly earns its place: less often than the pitch suggests
WASM pays off with heavy computation, bulk data and few boundary crossings. Rewriting an O(n) JavaScript loop as WASM usually buys slower loading and a harder stack to debug.
Stop guessing the header height: measure it with ResizeObserver
A wrapping navigation, late webfonts and changing content all break constants written into CSS. A ResizeObserver watches the element and writes the measured value back into a variable.
Full-text search on a static site: index at build time, no backend at runtime
A static site does not need Algolia to have search. Generate the index during the build, run the query entirely in the browser, and pay a few seconds of build time.
Pushing first-load JS to zero with Astro islands
From "async import" to "hydrate on demand" — what each of the three lazy-loading layers actually solves, with the code that runs on this site.
One source for head: SiteHead, JSON-LD escaping and hreflang
All three HTML shells render one component that emits every head tag, with the data assembled on the server. Plus a detail worth remembering: angle brackets in JSON-LD must be escaped.
Shipping a static site is not "upload the folder": caching, compression, honest 404s
Almost every static-hosting bug lives in response headers. Which files may be cached forever, which may not be cached at all, and why a static site must never use an SPA fallback.
Pagination at build time: why a static site should not page with a query string
The post list is sliced into static pages during the build, so page N is a real route. No client-side paging logic, no query string, just links.
No client:only for app pages: a static shell plus an explicit mount
Game and tool pages are a shell plus createApp, not client:only. I own the loading, ready and error states, and the retry button actually works.
