Tirup Mehta
Tirup Mehta
WritingMy Site Felt Slow, So I Fixed the Three Things Actually Slowing It Down

My Site Felt Slow, So I Fixed the Three Things Actually Slowing It Down

Performance/29.Jul.2026/4 min read
#performance#frontend#build-log
← All Articles
TL;DR

My portfolio looked fast and felt slow. Three real fixes on tirup.in and this blog (yielding long tasks, metric-matched fonts, on-demand code), plus the uncomfortable truth about whose devices your optimizations are really for.

Here's an embarrassing story. I rebuilt my portfolio to look fast: dark theme, clean type, minimal design. Then I opened it on a mid-range phone and it felt slow. Buttons responded late. Text jumped as the page loaded. The design was minimal; the engineering underneath wasn't.

Before the fixes, the insight that reframed everything: I had been optimizing for my own laptop. Fast machine, fast wifi, warm cache: the holy trinity of developer self-deception. Most of the world browses on cheaper phones over worse networks, and every millisecond I couldn't feel on my machine was fully visible on theirs. Performance work is empathy work with a profiler. Once that landed, the three fixes below stopped being chores and started being obvious.

Fix 1: Let the chef breathe

The browser is a single chef in a kitchen (the "main thread") doing almost everything: taps, scrolls, animations, your code. Hand the chef a two-hour recipe (one long data-parsing task) and nobody else gets served. My code did exactly that, so the page froze in little bursts. It felt "janky" because it was janky: taps queued behind my vanity data processing.

The fix: chop big jobs into slices and let the browser serve others between them, using its built-in polite-interruption signal:

utils/concurrency.ts Shipped On My Sites
// Slice a big job so the page stays responsive between chunks.
export async function processChunks<T>(items: T[], processFn: (item: T) => void): Promise<void> {
  let lastYield = performance.now();
  for (const item of items) {
    processFn(item);
    if (performance.now() - lastYield > 16) {  // one frame's budget
      await yieldToMain();  // scheduler.postTask, or MessageChannel fallback
      lastYield = performance.now();
    }
  }
}

Why 16 milliseconds? Screens redraw roughly every 16ms. Finish your slice inside that window and motion stays silky; spill over and frames drop. The number isn't arbitrary. It's the physics of the display you're serving.

Fix 2: The jumping text

The page loaded, then every headline jumped a few pixels. Cause: my custom font arrived late and measured letters slightly differently than the fallback, so the browser re-laid-out every line. The fix: teach the fallback font the exact measurements of the real one, making the swap geometrically invisible:

styles/typography.css Shipped On My Sites
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  ascent-override: 90.44%;
  descent-override: 22.56%;
  size-adjust: 107.4%;
  /* Fallback now occupies identical space. Swap = zero movement. */
}

Same principle everywhere: images reserve space before loading, modals reserve scrollbar space. Layout shift is just the browser discovering geometry late. So tell it early. My shift score is effectively zero now.

Fix 3: Stop shipping the whole kitchen

My bundle included a heavy chart library 95% of visitors never opened. Everyone paid; almost nobody benefited. Now heavy components load only on request behind a shimmer placeholder. The general rule I took from this: your bundle is a tax levied on every visitor to fund features most will never use. Audit it like a budget, because your cheapest phone user experiences it as one.

When NOT to optimize (the part most posts skip)

Three honest caveats. First, premature optimization is still real: I didn't touch any of this until Lighthouse and real taps proved a problem: "feels slow on a mid-range phone" is a measurement, "might be slow" is anxiety. Second, there's a floor: below ~100ms response, humans can't perceive improvements, so shaving 40ms to 20ms is theater. Optimize until it's imperceptible, then go build features. Third, the biggest wins are usually deletions. A removed library beats an optimized one every time. My fastest code on this blog is code that doesn't exist.

How I check my work

Chrome's built-in Lighthouse panel on every preview deploy: three numbers: appears fast, responds fast, nothing jumps. Any regression blocks the change. Run it on any site in a minute, including this one, right now.

The Takeaway
  • Test on the worst device you can find, not your laptop. Performance is empathy with a profiler.
  • Slice work to frame budgets, reserve all geometry up front, tax visitors only for what they use.
  • Stop at imperceptible. Delete before you optimize. Measure before you believe.