Cut home load time from ~60s to 2s for returning users on 3G.

Role
Staff Engineer
Period
2019 — 2024 (solo dev → consultant → Staff Engineer)
Team
7
Stack
Angular · NgRx · Laravel · MySQL

TL;DR

Users kept opening the app at the gym on 3G and waiting nearly a minute for the home to finish loading. The slowest request was dragging the whole screen with it. I indexed the join that didn't have one, moved the shared sections behind a cache, reworked the lazy loading, and stopped refetching the home on every return. Got it to 2 seconds on reliable connections.

Context

Bit Health is a training app. The home is the screen you land on after login, and it's where everything happens: continue-watching, recommendations, recently added content, categories. Most users open the app at a gym or a public park. Wifi isn't a given. Every decision downstream has to assume 3G.

The challenge

The home was firing several parallel requests on every open. For a brand new user it held up. But for an active user, someone with 'continue from where you stopped' populated, the slowest request dragged the whole screen with it. We were seeing close to a minute to get to a fully-loaded home. Users were bouncing before we had anything to show.

What we did

  1. 01

    Indexed the join that was slowing us down

    The 'continue watching' query joined the user progress table on user_id and content_id. Neither column was indexed. Every open was hitting the database with a query it had no way to answer fast. I added the missing index and pruned the SELECT so we only fetched the columns the home actually renders: thumbnail, title, progress percentage. That single change brought the slowest request under control.

  2. 02

    Separated shared content from personalized content

    Part of the home is the same for every user at a given moment: recommendations, new releases, categories. None of it needed to be recomputed per request. I moved those sections behind a cache with a scheduled refresh, and left only the personalized calls hitting the database per user.

  3. 03

    Reworked lazy loading on the client

    Angular was bootstrapping modules the home didn't need. I restructured the route tree so the home loaded only what the home renders. Settings, profile editing, and purchases became lazy chunks, loaded only when the user went there.

  4. 04

    Stopped refetching on return

    Our NgRx setup was naive. Every time the user came back to the home, we refetched everything from scratch. Fresh, yes. But slow. I changed the home to read from the store by default. When the user finished watching a video, I kicked the refresh off in the background during the redirect, so by the time they arrived on the home the store was already up to date. For actions that only changed local state, like favoriting a video or updating progress, I patched the store directly and skipped the network round-trip entirely.

Results

15s → 2s

Home load time

on reliable connections (87% reduction)

~60s → 2s

3G returning users

resolved the near-minute load complaint

~3×

Demo-to-contract conversion

reported by co-founder after industry event demos (1 in 10 → 3 in 10)

Looking back

  • We kept the multi-endpoint architecture because each endpoint was shaping its own response: thumbnail URL enrichment, category formatting. A single aggregator endpoint, with shaping done in one place, would have been faster and simpler to reason about. I'd push for that approach earlier next time.
  • Every card on the home was joining the media table to fetch its thumbnail URL. That was an N+1 we never solved. Eloquent has eager loading precisely for this case, and if I'd audited the ORM calls and fixed the relation loading, I believe we'd have brought the home under 500ms. I didn't invest the time at the moment.