Client-Side Web Applications

Rendering & Client/Server Boundaries


Learning Objectives

  • You can distinguish rendering strategies and explain the practice application’s client-rendered behavior.
  • You can place browser-only work in an appropriate lifecycle.
  • You can distinguish public configuration, per-user state, and server-only secrets.

The marketplace project contains code for different environments. A .svelte file can participate in both browser and server rendering. Values bundled into browser code are public, regardless of where their source files live. Rendering vocabulary helps us place that code while preserving the marketplace architecture.

Three useful rendering terms

Client-side rendering constructs or updates the application view in the browser. Server-side rendering produces application HTML on a server for a request. Prerendering generates HTML during a build for routes that can be prepared in advance.

These are not mutually exclusive labels for an entire technology. A framework can combine them for different routes or phases. A server-rendered page may later become interactive in the browser. The appropriate choice depends on content, interaction, freshness, and deployment requirements.

The SvelteKit page-options reference documents the controls. We preserve the working scaffold rather than trying every combination now.

What hydration actually means

When application HTML was rendered on the server, hydration establishes the client-side application behavior over that existing structure. It is not simply another word for every JavaScript page update.

The supplied root +layout.ts has ssr = false. For the marketplace client, the initial application document is therefore a shell and the browser renders the application. Do not describe the marketplace’s database-backed listing content as already server-rendered in that initial document.

Turning off page SSR does not eliminate the development server, the build process, or SvelteKit’s framework-generated modules. Part 1’s app.html and sync lifecycle remain necessary. Runtime, rendering strategy, and application responsibility are different dimensions.

Observe document and live DOM separately

Open the practice application with the Network panel. Inspect the original document response and compare it with the live Elements tree after JavaScript runs and you click Load todos.

Which content came with the document? Which appeared after a later API request? Temporarily disable JavaScript and reload, then re-enable it after recording the result. The practice and marketplace clients use the same client-rendered arrangement: a full interactive page is not promised without JavaScript. Record that limitation rather than mistaking the initial shell for a broken database.

Part 4 demonstrates progressive enhancement with a separate neutral exercise starter. Part 9 makes resilience a deeper architectural concern. Neither changes the marketplace’s current browser-to-Hono read path.

Compare the document evidence

The following normalized excerpts record the relevant evidence from two local pages. Framework bootstrapping details have been omitted so that the comparison stays focused on the returned document.

The client-rendered page’s document contains an application container and scripts, but not the application headings or todo data:

<body>
<div style="display: contents"></div>
<script src="/_app/immutable/entry/start.js"></script>
</body>

After its browser code and later data request run, the live DOM can contain content that was absent from that response:

<main>
<h1>Walking skeleton</h1>
<section aria-labelledby="todos-heading">
<h2 id="todos-heading">Todos</h2>
<ul>
<!-- List items inserted after the API response. -->
</ul>
</section>
</main>

A server-delivered comparison response already contains its public catalog content:

<main>
<h1>Course catalog</h1>
<ul>
<li>WEB: Web software</li>
<li>DATA: Working with data</li>
</ul>
</main>

Compare these excerpts with the document response and live DOM you observed in the practice application. The last excerpt establishes that the catalog content arrived in the document rather than depending only on a later browser fetch. The document alone does not establish whether the HTML was produced for this request or prerendered earlier; that distinction also requires configuration or server evidence.

You have not yet learned to write server loaders. Part 4 supplies a bounded exercise starter for a working server-handled form. Part 9 makes server loading and its identity boundary an implementation subject.

A headline without scripting

0 / 5 points

A news page’s document response already contains its article headline. The same headline remains visible after scripting is disabled and the URL is reloaded. What does this observation support?

Browser-only work belongs to a browser lifecycle

A module evaluated on a server does not have a browser window or document. Work such as measuring the viewport or attaching a window listener should run in a browser lifecycle and clean up when removed.

Return to the practice application (practice/) and create client/src/lib/examples/ViewportWidth.svelte. Mount it on /lab:

<script lang="ts">
import { onMount } from "svelte";
let width = $state<number | null>(null);
onMount(() => {
const update = () => {
width = window.innerWidth;
};
update();
window.addEventListener("resize", update);
return () => window.removeEventListener("resize", update);
});
</script>
<p>Viewport width: {width === null ? "not measured yet" : `${width}px`}</p>

The initial value does not invent a server measurement. The listener is paired with its cleanup. onMount itself is synchronous even when code started inside it later awaits a promise. See Svelte lifecycle hooks.

There are also declarative helpers for common browser interactions. This explicit example exists to expose the environment/lifetime issue, not to require manual listeners everywhere.

Measure the viewport at the right time

Use the browser lifecycle to measure both viewport dimensions. The supplied component deliberately reads browser state too early and fails to clean up its listener. Correct those boundaries and verify that removal stops the listener. There is no new server loader to implement in this exercise.

Pair a browser measurement with cleanup

0 / 10 points

Task

Repair src/lib/ViewportSize.svelte. Its current version reads browser globals during initialization and attaches an anonymous listener with no cleanup.

Requirements

Begin with an unmeasured value. In a synchronous onMount callback, measure both window.innerWidth and window.innerHeight. In a paragraph with role="status", display Viewport: <width> × <height> (<orientation>), where orientation is landscape when width is greater, portrait when height is greater, and square when they are equal. Update both dimensions and the classification after a resize event. Return cleanup that removes the very same listener function. Before the browser measurement, the status text is Viewport: not measured yet.

Do not read window at module/component initialization or put the measurement in shared mutable module state. Do not add a timer or a request. The task asks for the explicit lifecycle pattern taught here, rather than a new framework helper. Tests can check listener cleanup without proving every real device’s viewport behavior.

Submit

Submit only src/lib/ViewportSize.svelte. Measurement, classification, and updates earn 5 points; correct listener cleanup earns 5. The dimension check includes all three orientation boundaries. Also inspect the component locally at two browser sizes using the chapter’s lab page.

Shared server state is not a user’s browser state

Imagine a mutable module variable holding the “current user’s filter.” In a browser-only process it might appear to work for one person. On a server, the module can be shared across requests. One user’s values could then affect another user.

Even with SSR disabled in the marketplace client, avoid establishing unsafe patterns that would fail when an application grows. Put per-request data in request context and per-component data in a component/layout instance. Share immutable definitions freely; treat shared mutable user state as a deliberate architecture decision.

The SvelteKit state-management guidance discusses this boundary.

Public configuration is intentionally public

The practice application uses VITE_API_BASE_URL in browser source. It is an address a browser needs to contact, not a credential. Vite exposes VITE_-prefixed values to client code; inspect the environment-variable guidance.

Do not put a database password, session signing key, or private service token in such a variable. Hiding a value from visible page text does not remove it from downloaded JavaScript. Likewise, obfuscation is not secret storage.

The API reads database settings on the server side. Browser code requests allowed representations through HTTP instead of receiving a PostgreSQL connection string.

A client environment variable

0 / 5 points

A client uses import.meta.env.VITE_API_BASE_URL. Someone proposes adding VITE_DATABASE_PASSWORD because its value is never shown as page text. Which response is appropriate?

The marketplace keeps one application API

SvelteKit can execute server-side handlers and data operations. That capability does not oblige us to use it for the marketplace’s business rules. Figure 1 shows the marketplace project’s runtime path.

Course diagram
Figure 1. The browser reaches PostgreSQL through the marketplace’s Hono HTTP API.

Learning the boundary explicitly makes methods, validation, status codes, and authority visible. Part 4 introduces a typed HTTP client for this same API. We will evaluate what the abstraction changes in source code and what it leaves unchanged at runtime.

The working Deno toolchain does not by itself establish that every deployment adapter or hosted SSR arrangement has been validated. Keep the local rendering behavior separate from provider claims. Hosted deployment remains a walkthrough, not a prerequisite for this part.

Check Your Understanding

  1. How do SSR, client rendering, and prerendering differ?
  2. Why is hydration not an accurate description of every client-rendered shell?
  3. What still runs on a server when page SSR is disabled in development?
  4. Why is a mutable module variable dangerous for per-user server data?
  5. Why must a browser-visible configuration value never contain a secret?