Request Lifecycle: How Web Frameworks Handle a Request · Spring, the Browser Side, and Putting It Together

The Browser Side: React and Vue Page Loads

Why a dashboard took four seconds to show data despite fast APIs, and what happens in the browser between navigation, rendering, hydration and data fetching in React and Vue apps.

Scenario

Example app: a React (or Vue) dashboard renders a Dashboard component that fetches the user in an effect, then renders Projects, which fetches projects for that user in its own effect, then ProjectStats, which fetches stats for each project. Each API answers in about 300 ms.

What you see in production

  • The page shows spinners for more than 2.5 seconds on a fast connection.
  • The network panel shows requests starting one after another, in a staircase (a waterfall).
  • APIs are fast; the time is spent waiting for each component to render before it starts its own request.

The naive code

function Dashboard() {
  const [user, setUser] = useState(null);
  useEffect(() => { fetch("/api/me").then((r) => r.json()).then(setUser); }, []);   // starts AFTER render
  return user ? <Projects userId={user.id} /> : <Spinner />;
}
// Projects and ProjectStats repeat the same pattern

Reproduce it

The simulation models the page-load timeline: HTML, the JavaScript bundle, first render or hydration, then data requests started by effects in nested components, compared with starting all independent requests together at the route level.

Effect waterfalls vs route-level parallel loadingTry it yourself
const t = { html: 200, js: 900, render: 150, api: 300 };

function waterfall() {
  let clock = t.html + t.js + t.render;                 // effects run after the first render or hydration
  const steps = [];
  for (const name of ["/api/me", "/api/projects", "/api/stats"]) {
    clock += t.api;                                      // each request waits for the previous render
    steps.push(`${name} done at ${clock} ms`);
    clock += t.render;
  }
  return { clock, steps };
}

function routeLevel() {
  // the route loader (or a server component) starts all independent requests as soon as navigation begins,
  // in parallel with downloading JavaScript; dependent data is fetched together on the server
  const dataReady = t.html + Math.max(t.api * 2, 0);    // me -> projects+stats as one server-side query
  const clock = Math.max(dataReady, t.html + t.js) + t.render;
  return { clock, steps: [`data ready at ${dataReady} ms, JS ready at ${t.html + t.js} ms`] };
}

for (const [label, fn] of [["effects in nested components", waterfall], ["route-level parallel loading", routeLevel]]) {
  const { clock, steps } = fn();
  console.log(`${label}: content visible at ${clock} ms`);
  for (const s of steps) console.log(`   ${s}`);
}

Output

effects in nested components: content visible at 2600 ms
   /api/me done at 1550 ms
   /api/projects done at 2000 ms
   /api/stats done at 2450 ms
route-level parallel loading: content visible at 1250 ms
   data ready at 800 ms, JS ready at 1100 ms

Root cause

In the browser, a component's effect (useEffect in React, onMounted in Vue) runs after that component renders. When each component fetches its own data in an effect and renders its children only when the data arrives, every level of the tree waits for the previous level: a request waterfall.

The lifecycle in the browser

  1. Navigation: DNS, TLS and the HTML request (Navigation Timing exposes each phase).
  2. The browser parses HTML and downloads CSS and JavaScript.
  3. Render: for a client-rendered SPA, React or Vue renders the app into an empty root. For server-rendered apps (Next.js, Nuxt, Remix), the HTML already contains content and the framework hydrates it, attaching event handlers (hydrateRoot). If server and client output differ, you get hydration mismatch warnings.
  4. Effects / mounted hooks run (useEffect, onMounted), often starting data requests.
  5. Client-side navigation: the router changes the URL without a full reload. Vue Router runs navigation guards (beforeEach, beforeResolve, afterEach), and React frameworks run route loaders or fetch server components.

The fix

  • Fetch data at the route level, before or while rendering: route loaders (React Router, TanStack Router), server components (Next.js), useFetch/asyncData in Nuxt, or Vue Router beforeResolve guards.
  • Start independent requests in parallel (Promise.all), and combine dependent ones on the server into one endpoint or query.
  • Use a data-fetching library (TanStack Query, SWR, Pinia Colada) for caching and deduplication, and prefetch on link hover or visibility.
  • Avoid fetching in effects for data the page always needs; see You might not need an effect.

Trade-offs

  • Route-level loading couples routes to data needs, but makes loading visible and parallel.
  • Server rendering improves first content but adds server cost and hydration complexity.

Detect it in production

  • Real-user monitoring: Largest Contentful Paint and time to data-ready.
  • Look for staircases in the network waterfall; count sequential round trips on the critical path.

Key points

  • In the browser: navigation → HTML → JS/CSS → render or hydrate → effects → data → re-render.
  • Effects run after render, so nested fetch-in-effect components create waterfalls.
  • Load data at the route level, in parallel, and prefetch.

Practice questions

What is hydration in React or Vue server-side rendering?

Answer

The browser receives server-rendered HTML; the framework then runs the same components on the client and attaches event handlers to the existing DOM instead of recreating it. Differences between server and client output cause hydration mismatch errors.

What is a request waterfall in a single-page app, and how do you avoid it?

Answer

Sequential requests where each component starts fetching only after its parent rendered with data. Avoid it by loading data at the route level (loaders, server components), running independent requests in parallel, combining dependent data on the server, and prefetching.

What are Vue Router navigation guards and when do they run?

Answer

Hooks that run during client-side navigation: global beforeEach, per-route beforeEnter, in-component guards, global beforeResolve (after async components and in-component guards resolve) and afterEach. They're used for auth redirects and loading data before a route renders.

Open in the app to mark lessons complete, take notes and track your progress.