ANDREWHUNT
Home

Projects & toysCase study

Everything on one page. The right things first.

I led the redesign and near-complete rebuild of Zip’s Request Details Page (RDP), its busiest page at 10,000+ daily views. This case study focuses on the loading architecture: reusing data from the previous screen and prioritizing the content people needed to act.

Reported p90 time to usable, by entry point. Baseline and measurement notes.

Direct load
6.8s to 4.6s
From Requests Search
5.3s to 2.1s
From Approvals Search
5.9s to 2.8s

Frontend performanceTechnical leadershipZip · 2023 · 5 months · project lead · 2 designers, 1 PM, 5 engineers (3 frontend)

01 · Context

The Request Details Page sat at the center of a complex enterprise workflow

A in Zip tracks a purchase through approvals. Its details page brings together pricing, billing, documents, vendor information, and the conversations and decisions around that purchase.

How people got to the page mattered a lot. A starts with no request data in the in-memory client cache. Someone coming from the or pages already has some of the request’s data on the client.

Where a day’s views came from
Share of daily views by entry point
Arriving fromShare of views
Direct load30%+
From Requests Search30%
From Approvals Search15%
From other pages~25%

The trigger

The old page made customers bounce between tabs to complete a single workflow. The redesign brought those sections onto one long, data-heavy page: less tab hopping, browser Find across sections once their content has loaded, and a comments sidebar beside the information being discussed.

We kept the tabs in the UI, but as links that auto-scroll to that section. That also meant other pages could (and did) link directly into a specific section. The catch: a page that shows everything at once still has to feel fast.

The original loading model

The old page ran one large query for the header and the data shared across tabs, plus a second, tab-specific query for everything else that tab needed (lower-priority data included). Both queries ran concurrently, but request content waited for both responses. Tabs at least kept each fetch smaller than the whole page, but the loading strategy followed the UI structure, not what the user actually needed first. It also didn’t reuse cached data well: if you came from Requests Search, the client already had a bunch of the request’s fields, and the page waited for its own queries anyway.

Consolidating the page didn’t solve those waits on its own. The before/after figures below compare the initially slow new page with that same redesigned page after optimization. Most old tab routes had also been similarly slow, but that is context, not a separate measured baseline.

The challenge was deciding which info had to be interactive first, what we could reuse from the previous screen, and what could load separately without blocking that first interaction.

02 · Approach

Define performance around the user’s first useful interaction

We defined “time to usable” around a concrete milestone: the header, details panel, and first two relevant sections were interactive. The destination link determined which sections mattered – the top of the page was the default. Rendering some cached text was a head start, not completion of that milestone. We compared direct loads and client-side arrivals from search separately.

Where it got hairy

Two things made this more complicated than just splitting up a query:

  • “Usable” depends on the link. People come to the RDP to interact with different parts of it, and links from around Zip drop them into different sections. If you clicked a link to the Documents section, you want Documents first. So the definition of “usable” changed depending on the link, and the top of the page was just the default.
  • The previous page determines what is already cached. Requests Search fetched request objects. Approvals Search fetched a smaller set of fields for each approval’s linked request. A bill or vendor detail page could also leave related request data in ’s cache before navigation to the RDP.

Supporting every possible combination would have been a mess of loading states and dependencies between fields. So we balanced query complexity against performance by supporting a bounded set of used by the biggest referrers. Complete supported field groups could render from cache while separate operations fetched the remaining data. A missing or unsupported group still needed a fetch – we didn’t try to handle every possible partial shape.

Fragments describe field selections. Apollo’s normalized object identities make fields reusable across queries. This was a change to query boundaries and rendering gates, not automatic removal of cached fields from a network request.

Cache reuse explains earlier rendering, not freshness. Data from the previous screen could still be stale, and a partial request was not enough to declare the destination ready. This account does not reconstruct the original refresh, invalidation, or approval-action safeguards.

Three entry points, three strategies

  • Direct load30%+ of views · No request data in the in-memory client cache

    Fetch the data the destination needs for its first interaction. Other sections no longer have to hold up that milestone.

  • From Requests Search30% of views · Most of the top of the page already cached

    Render supported field groups from the request search result, while separate queries supply the remaining data needed for the first interaction.

  • From Approvals Search15% of views · Part of the request already cached

    The approval result includes a smaller set of fields for its linked request. Reuse supported cached fields and fetch the remaining critical data.

For a default arrival at the top of the page, these sections could sit outside the initial readiness requirement. A direct link to Documents promoted Documents into the critical set. “Deferred” meant not blocking that first interaction, not unimportant or guaranteed to be out of view:

  • Documents
  • Vendor details
  • Payment
  • Approval attributes
What readiness depends on

Illustrative dependencies – not to scale and not a trace. Arrows show what readiness depends on, not when requests start. Historical totals are shown separately in the results.

Before optimization · new consolidated page

  1. Page queries include data beyond the first interaction
  2. Required responses hold up usable request content

After optimization · direct load

  1. Fetch critical data for the destination section
  2. Header, details, and first relevant sections become usable

Other sections load outside this readiness requirement.

After optimization · from Requests Search

  1. Render supported cached fields while fetching remaining critical data
  2. Header, details, and first relevant sections become usable

Other sections load outside this readiness requirement.

The original per-section scheduling triggers are not available here. These sketches do not imply that every deferred request started after readiness, or that the implementation used GraphQL @defer.

03 · Validation

Measure locally, verify in production

I checked the loading change from several angles: browser waits, query and cache behavior, component renders, and production usage.

Tools used to validate the changes
Chrome DevTools
The browser's built-in profiler: network waterfall, main-thread activity, paint timings.Where a cold load spends its time
Apollo
Apollo Client dev tools: queries in flight and the state of the cache.Which queries ran, when, and what came from cache
Datadog
Production monitoring: latency and error dashboards per query, from real traffic.Backend latency in production, per query
React Profiler
Records which components rendered, how often, and why.Whether splitting queries introduced avoidable component re-renders
FullStory
Session replay and product analytics.How people actually used the new page

Leadership scope

I led the project: 2 designers, 1 PM, and 5 engineers (3 frontend) over ~5 months in 2023. As an IC, I contributed 50% of the frontend features. This query optimization was one part of that work. A big part of the job was lining up the product’s information hierarchy with the loading strategy, so that what users needed first was an explicit engineering priority instead of an accident of tab boundaries.

Decisions I owned:

  • Reusing search data. Treating the previous screen as a data source, so navigating from search doesn't behave like a cold page load.
  • The query boundaries. Which data is critical for the first interaction, and which sections can arrive later.
  • The workflow diagram. A full frontend rewrite of the diagram that shows a request's path through its approvals. It implemented the redesign, but also rendered faster than the old library and made the diagram more central to the page.

Rollout

We launched over ~3 months: customers first, then an opt-in beta, then non-enterprise customers, then everyone. Many customers asked to be switched over early after hearing about the new version.

I pushed for product tours for the launch, and built the FullStory dashboards we used to track adoption.

Outcome

The reported results show the largest reduction for arrivals from Requests Search, where cached fields could be reused. Direct loads improved by about a third but still took 4.6 seconds at p90. Query splitting left critical request data on that path. Reaching the readiness milestone sooner did not make the remaining network work disappear.

Reported p90 time to usable by entry point, in seconds
Reported p90 time to usable by entry point, in seconds
Arriving fromNew page before optimizationNew page after optimization
Direct load6.8s4.6s
From Requests Search5.3s2.1s
From Approvals Search5.9s2.8s

Historical figures, not revalidated from original data. Measurement scope and limitations.

What my team said

  • We were having performance issues right before launch, and Andrew took the initiative to break queries into smaller pieces and delivered a significantly better user experience.

    Engineering manager, 2023 review
  • When there was some design churn he took on basically all the burden of the work and really protected the rest of the team from it.

    Peer engineer, anonymous 2023 review

04 · Takeaways

Three tradeoffs I’d carry forward

Making the page usable sooner meant drawing explicit boundaries, not making every section load as quickly as possible.

  1. Give cache reuse a boundary.Supporting a few known field selections keeps reuse understandable. Each additional partial-data shape is another case to reason about – more cache coverage is not free.
  2. Critical follows the destination.Documents can wait on a general visit, but not when a link takes someone there to read a document. Screen position alone cannot decide what is critical.
  3. Cold loads have a different constraint.With no request data in memory, the page still needs to fetch essential fields. The remaining direct-load time is a separate optimization problem – cache reuse on other paths cannot solve it.