# Rendering modes

> Choose how each page renders - in the browser only, once at build time, or on every request - and mix all three in one app.

Source: https://gyral.dev/docs/rendering-modes/

A Gyral component is the same code wherever it renders. What changes is **when** its HTML is
made, and that is a choice you make per page, not per app:

| Mode            | HTML is made                  | Needs at runtime        | Use it for                                 |
| --------------- | ----------------------------- | ----------------------- | ------------------------------------------ |
| **Client**      | in the browser, by JavaScript | any static file host    | widgets inside an existing page, admin UIs |
| **Static**      | once, at build time           | any static file host    | home, docs, marketing, blog posts          |
| **Per request** | on the server, for each visit | a server (Node, Bun, …) | carts, accounts, search results, forms     |

The static and per-request modes produce the **same HTML**: a static page is a per-request page
rendered ahead of time. Both work before any JavaScript loads and hydrate in place when it does,
so you can move a page from one mode to the other without touching its components.

## Mixing modes

Most real sites mix them. This website prerenders every page and runs two live components: the
counter on the home page and the search box on the search page. [gyral-shop](https://github.com/gyraljs/gyral-shop) renders product
listings and the cart per request, because they change with every visitor.

Keep the decision next to your routes, so there is one place to read it:

```ts
// src/routes.ts
import { routes } from '@gyral/router';
import type { RenderMode } from '@gyral/ssr/static';

export const site = routes({ home: '/', about: '/about', account: '/account' });

/** How each route renders in production. */
export const modes = {
  home: 'ssg',
  about: 'ssg',
  account: 'ssr',
} as const satisfies Record<'home' | 'about' | 'account', RenderMode>;

/** Every route rendered at build time, for the prerender step. */
export const staticPaths = (): string[] =>
  (Object.keys(modes) as (keyof typeof modes)[])
    .filter((name) => modes[name] === 'ssg')
    .map((name) => site.href(name, {}));
```

`RenderMode` names them the way the rest of the ecosystem does: `ssg` (static), `ssr` (per
request) and `csr` (client). Gyral's
[isomorphic example](https://github.com/gyraljs/gyral/tree/v0.3.10/examples/isomorphic) uses
exactly this table.

## Client-only builds

An app that no server renders can tell the build so, and leave the hydration code out of its
bundle:

```ts
// vite.config.ts
import { defineConfig } from 'vite';
import { gyral } from '@gyral/core/vite';

export default defineConfig({
  plugins: [gyral({ clientOnly: true })],
});
```

With `clientOnly: true` the browser bundle carries no hydration code: no seed reading and no
hydration chunk, and when your app has no other `import()`, Vite's preload helper goes too. The
fallback for [invoker commands](https://gyral.dev/docs/intent/#invoker-commands) stays only when a module may make
a component listen for `command` intents ([what the build reads](#what-the-build-reads)).
Measured on Gyral's examples (gzip, 0.3.10):

| App         | Default: first load | Default: all chunks | `clientOnly: true` |
| ----------- | ------------------- | ------------------- | ------------------ |
| hello-world | 9.70 KiB            | 12.15 KiB           | 8.60 KiB           |

"First load" is what a page downloads before any lazy `import()`; in these examples a
client-only build has nothing lazy left, so its first load is all of it.

Use it for apps that load their components with a `<script type="module">` into pages no Gyral
server wrote: widgets in an existing site, admin tools, single-page apps. Leave it off as soon
as any page is server-rendered or prerendered, including islands, and give your Vitest config
the same `gyral({ clientOnly: true })` so tests build the way the app does. List plugins of
your own beside it: `plugins: [gyral({ clientOnly: true }), mine()]`.

If server-rendered markup reaches a client-only build anyway, nothing breaks twice: each
component drops the server's seed and DOM and renders fresh from its attributes, so a view is
never doubled. Development warns once and names `clientOnly`; production renders fresh
silently. The server's work is wasted, though, so treat that warning as a configuration bug.

### Starting from create-gyral

`create-gyral`'s `basic` template is client-only from the start: its `vite.config.ts` sets
`gyral({ clientOnly: true })`, and its `vitest.config.ts` reuses that config, so tests
build the way the app does. A new app is about 8.5 KiB gzip, in one file. Keep the option while
every page renders in the browser. Delete `clientOnly: true` as soon as any page is rendered on
a server or prerendered, for example when you add `@gyral/ssr`: with it on, those components
can't hydrate and render again from scratch. The `ssr` template builds without it.

### What the build reads

To decide what a bundle needs, the build reads the code that can affect your components, not
your whole dependency tree:

- **Which modules**: your own source (everything outside `node_modules`, workspace and linked
  packages included), and installed packages that are Gyral packages or list a `@gyral/*`
  package in `dependencies`, `peerDependencies` or `optionalDependencies`, directly or through
  their own dependencies, such as a design system built on Gyral. Any other package can't
  define a component or write its spec, so it isn't read.
- **What counts**: the parsed source, not its text, so comments and type-only code never count.
  The invoker fallback stays when a module's markup has `command` in a `data-intent-on` list
  (`data-intent-on="command"`), a `data-intent-command` attribute or a bound `data-intent-on`,
  when it has the string `'command'` on its own (a `setAttribute('data-intent-on', 'command')`), or when it uses `raw` imported from `@gyral/core` (under any alias, or
  through a module that re-exports it), whose markup is only known at run time. A function of
  another package that happens to be called `raw` doesn't count.

Every build with `gyral()`, client-only or not, uses the same scan for the features a spec
field turns on: view transitions, the frame lane, custom states, form association,
subscriptions and the error fallback are bundled only when a module names `viewTransition`,
`renderOnFrame`, `states`, `formAssociated`, `subscriptions`, `error` or `Errored` in code.
Write these field names literally: a name built at run time isn't seen, and the component then
renders without that feature (development builds warn). A package that writes these fields for
your components must depend on Gyral to be read: a component library lists `@gyral/core` as a
peer dependency in its `package.json`
([Publishing a component library](https://gyral.dev/docs/packages/#publishing-a-component-library)).

This site server-renders its pages and hydrates two islands, so it builds without the option.

## Where to go next

- **Client only**: write components as in [Getting started](https://gyral.dev/docs/getting-started/) and load
  them with a `<script type="module">`. No server code at all, and a
  [client-only build](#client-only-builds) to match.
- **Static**: [Static sites and prerendering](https://gyral.dev/docs/static-sites/).
- **Per request**: [Server rendering](https://gyral.dev/docs/server-rendering/).
- **Shipping it**: [Deploying](https://gyral.dev/docs/deploying/) covers static hosts, Node, Bun and edge
  runtimes.
- **A Rust backend**: [Rust servers](https://gyral.dev/docs/rust-servers/) covers each mode with Rust as the
  web server.
- **Less JavaScript on each page**: [Code-splitting and lazy loading](https://gyral.dev/docs/code-splitting/).
