theme | class | highlighter | drawings | transition | title | layout | |
---|---|---|---|---|---|---|---|
./theme |
text-center |
shiki |
|
slide-left |
Performant Websites with Remix |
intro |
I'm a UX designer and developer who makes exploratory prototypes with code. My pronouns are they/them.
- Simplicity
- Discoverability (SEO)
- Initial page load
- Do as much work on the server as possible
- Put your servers as close to your users as possible
- Send as little data over the network as possible
- Ship HTML to your users
- Make pages work before JavaScript loads
- Embrace web standards and browser defaults
- If you fetch your data on the client, you can't load images until it loads data, you can't load data until you load JavaScript, and you can't load JavaScript until the document loads
- The user's network is a multiplier for every single step in that chain 😫
- Always fetching on the server removes the user's network as a multiplier everywhere else
// Loaders only run on the server and provide data
// to your component on GET requests
export async function loader() {
return json(await db.projects.findAll());
}
// Actions only run on the server and handle POST
// PUT, PATCH, and DELETE. They can also provide data
// to the component
export async function action({ request }) {
const form = await request.formData();
const errors = validate(form);
if (errors) {
return json({ errors });
}
await db.projects.create({ title: form.get("title") });
return json({ ok: true });
}
- Long running servers and serverless functions tend to run their code in a single region
- Depending on your users, the one region could be a long way from where they are, which could mean a long time before the server can start fetching the data
- By utilizing an edge network, you can deploy your site on servers in dozens of regions around the world:
- Adapters!
import { createRequestHandler, handleAsset, } from "@remix-run/cloudflare-workers";
import * as build from "../build";
const handleRequest = createRequestHandler({ build });
const handleEvent = async (event: FetchEvent) => {
let response = await handleAsset(event, build);
if (!response) response = await handleRequest(event);
return response;
};
addEventListener("fetch", (event) => {
event.respondWith(handleEvent(event));
});
- You can deploy your entire site to the edge with something like Cloudflare Workers or Pages, or...
- You can utilize hybrid options with something like Vercel's config export:
app/routes/concerts.tsx
/concerts
```tsx
export const config = { runtime: 'edge' };
export function loader() { ... }
export default function Component() {
const { featured } = useLoaderData();
return (
<>
<h1>Concerts</h1>
<p>Featured concert: {featured}</p>
<Outlet />
</>
);
}
```
```tsx
export const config = { runtime: 'nodejs' };
export function loader() { ... }
export default function Component() {
const { featured } = useLoaderData();
return (
<>
<h1>Concerts</h1>
<p>Featured concert: {featured}</p>
<Outlet />
</>
);
}
```
app/routes/concerts.trending.tsx
/concerts/trending
export const config = { runtime: 'edge' };
export function loader() { ... }
export default function NestedComponent() {
const { trending } = useLoaderData();
return <ul>{trending.map(trend => <li>{trend.name}</li>)}</ul>;
}
- Mobile network
- Nested routing
Coming Soon:
Partial hydration with React Server Components
function loader() {
const { title, content } = await loadArticle();
return {
articleHeader: <Header title={title} />,
articleContent: <AsyncRenderMarkdownToJSX makdown={content} />,
};
}
export default function Component() {
const { articleHeader, articleContent } = useLoaderData();
return (
<main>
{articleHeader}
<React.Suspense fallback={<LoadingArticleFallback />}>
{articleContent}
</React.Suspense>
</main>
);
}
- Websites should work as soon as you can see them: progressive enhancement!
- Apps that are rendered entirely via JavaScript require waiting for the script(s) to download and execute before anything is displayed to users
HTML |---|
JavaScript |---------|
Data |---------------|
page rendered 👆
- HTML will download and display quicker than JavaScript and allows more parallel execution
👇 first byte
HTML |---|-----------|
JavaScript |---------|
Data |---------------|
page rendered 👆
- By default, Remix server-renders all routes
- You can opt out of this on a per-route basis using the new
clientLoader
andclientAction
exports along withHydrateFallback
... but you're still shipping HTML!
export async function clientLoader() {
const data = await loadSavedGameOrPrepareNewGame();
return data;
}
export function HydrateFallback() {
return <p>Loading Game...</p>;
}
export default function Component() {
const data = useLoaderData<typeof clientLoader>();
return <Game data={data} />;
}
- Displaying your app's shell UI the moment it loads and quickly replacing it with your first screen allows you to give your users the impression that your experience is fast and responsive
- The ideal
HydrateFallback
is effectively invisible to people, because it simply provides a context for your initial content - Your fallback should not an onboarding experience or a splash screen; its primary function should be to enhance the perception of your experience as quick to launch and immediately ready to use
- The HTML you export from your
HydrateFallback
should be nearly identical to the HTML on the initial screen of your app. If you include elements that look different when the data finishes loading, people may experience an unpleasant flash between the fallback and the initial screen of the app.
- Why? Everyone has JavaScript, right?
- The data layer of your site should function with or without JavaScript on the page
- It will make your website feel faster
- This is part of progressive enhancement
/**
* The public API for rendering a history-aware `<a>`.
*/
export const Link = ({ to: href, onClick, ref, target, ...props }) => {
// ...
return (
<a
href={href}
target={target}
onClick={isExternal || reloadDocument ? onClick : handleClick}
ref={ref}
{...props}
/>
);
}
/**
* A `@remix-run/router`-aware `<form>`.
*/
export const Form = ({ method, action, onSubmit, ref, ...props }) => {
// ...
return (
<form
method={method}
action={action}
onSubmit={reloadDocument ? onSubmit : handleSubmit}
ref={ref}
{...props}
/>
);
}
<form>
<link rel="prefetch">
- URLs for assets
Cache-Control
export default function Component() {
return (
<>
<h1>See New Concerts in Your Area</h1>
<Link to="concerts/new" rel="prefetch" />
</>
);
}
export default function Component() {
return (
<>
<PrefetchPageLinks page="/concerts/trending" />
<h1>All Concerts</h1>
<ul>...</ul>
</>
);
}
import type { HeadersFunction } from "@remix-run/node"; // or cloudflare/deno
export const headers: HeadersFunction = () => ({
"Cache-Control": "max-age=604800, stale-while-revalidate=86400",
});