Skip to content

Latest commit

 

History

History
executable file
·
683 lines (493 loc) · 18.1 KB

slides.md

File metadata and controls

executable file
·
683 lines (493 loc) · 18.1 KB
theme class highlighter drawings transition title layout
./theme
text-center
shiki
persist
slide-left
Performant Websites with Remix
intro

Performant Websites with Remix


layout: presenter presenterImage: profile.jpg

Hi! I'm Mark 👋

I'm a UX designer and developer who makes exploratory prototypes with code. My pronouns are they/them.


transition: none

Holotypes


Holotypes


What Are We Optimizing For?

  • Simplicity
  • Discoverability (SEO)
  • Initial page load

How Do We Achieve These Goals?

  1. Do as much work on the server as possible
  2. Put your servers as close to your users as possible
  3. Send as little data over the network as possible
  4. Ship HTML to your users
  5. Make pages work before JavaScript loads
  6. Embrace web standards and browser defaults

Do as much work on the server as possible

  • 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

Remix on the Server

// 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 });
}

Put your servers as close to your users as possible

  • 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:

Remix at the Edge

  • 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>;
}

transition: none

Send as little data over the network as possible

  • Mobile network

transition: none

Send as little data over the network as possible

  • Nested routing

transition: none

Send as little data over the network as possible

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>
    );
}

Ship HTML to your users

  • 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 👆

HTML in Remix

  • By default, Remix server-renders all routes
  • You can opt out of this on a per-route basis using the new clientLoader and clientAction exports along with HydrateFallback... 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} />;
}

transition: none

HydrateFallback User Experience

  • 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.

transition: none

HydrateFallback User Experience


HydrateFallback User Experience


Make pages work before JavaScript loads

  • 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

Progressive Enhancement in Remix

/**
 * 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}
        />
    );
}

transition: none

Progressive Enhancement in Remix

/**
 * 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}
        />
    );
}

Embrace web standards and browser defaults

  • <form>
  • <link rel="prefetch">
  • URLs for assets
  • Cache-Control

<link rel="prefetch"> in Remix

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>
        </>
    );
}

Cache-Control in Remix

import type { HeadersFunction } from "@remix-run/node"; // or cloudflare/deno

export const headers: HeadersFunction = () => ({
    "Cache-Control": "max-age=604800, stale-while-revalidate=86400",
});

layout: new-section