Skip to main content
Fresh lemon

Fresh 1.4 – Faster Page Loads, Layouts and More

In this cycle we’ve focused on the overall developer experience of making it easier to use shared layouts, route-specific islands and more in Fresh.

Remember: you can start a new Fresh project by running deno run -A -r https://fresh.deno.dev or update an existing project by running deno run -A -r https://fresh.deno.dev/update . in your project folder.

Faster page loads with ahead-of-time compilation

So far, Fresh has always compiled assets on the fly. This has served us well so far, as it enables lightning fast deployments with no build step, but we realised just-in-time (=JIT) rendering with large islands was noticeably slower. We arrived at a pre-compile solution that results in assets being served ~45-60x faster for a cold start of a serverless function, with minimal impact on deployment times. The savings depend on the size of the island, but even for small ones the improvements are very visible.

Here is a demonstration of the different techniques on the Fresh documentation site. The search box is a small island which weights around ~30kB.

The search box island revives nearly instantaneously with ahead of time compiled assets whereas it took 4.28s with JIT compilation.

Locally, when running the development server, Fresh will always use JIT compilation so that your sever can respond to API as soon as possible and doesn’t have to wait for asset compilation to finish.

You can opt into AOT compilation for deployments by following the ahead-of-time builds guide. Running deno task build will create a _fresh folder which holds all the generated assets.

Custom html, head and body tags

A tricky aspect in previous versions was setting the lang attribute on the <html>-tag. Up until now Fresh created the outer HTML structure up to the <body>-tag internally and you’d need to apply workarounds like creating a custom render function to modify the lang attribute.

await start(manifest, {
  // Old way of setting the `lang` attribute,
  // requires a custom render function :(
  render: (ctx, render) => {
    ctx.lang = "de";
    render();
  },
});

After thinking about this for a while we realised that we could simplify Fresh quite a bit by allowing you to render the HTML document yourself. So we did exactly that. With Fresh 1.4 you can set the <html> , <head> and <body>-tag directly on the server.

// routes/_app.tsx
import { AppProps } from "$fresh/server.ts";

export default function App({ Component }: AppProps) {
  return (
    <html lang="de">
      <head>
        <title>My Fresh App</title>
      </head>
      <body>
        <Component />
      </body>
    </html>
  );
}

To make upgrading from older Fresh versions easier, we added some logic to detect whether an <html>-tag was rendered or not. If none was rendered, we fall back to wrapping the html with the internal template like in past Fresh versions.

Layouts

A common aspect of building web applications is that many parts of the layouts are shared across routes. Think of the Header or Footer of a website which is the same component across routes. Previously, you could do that in routes/_app.tsx, but there was no way to go beyond that. Creating a shared layout for some sub routes in your app required extracting the code into a component and importing it into all routes manually.

In Fresh 1.4, we added support for _layout files, which can be described as a route local app wrapper. They can be put in any route folder and Fresh will detect all the layouts that match and stack them on top of each other.

routes/
  _app.tsx
  _layout.tsx
  page.tsx      # Inherits _app and _layout

  sub-route/
    _layout.tsx # Inherits _app and _layout
    index.tsx   # Inherits _app, _layout and sub-route/_layout
    about.tsx   # Inherits _app, _layout and sub-route/_layout

A _layout file looks very similar to a route file or the app wrapper. It uses the Component prop to continue rendering further layouts or the final route file.

// routes/_layout.tsx
import { LayoutProps } from "$fresh/server.ts";

export default function MyLayout({ Component }: LayoutProps) {
  return (
    <div class="my-layout">
      <h2>This is rendered by a layout</h2>
      <Component />
    </div>
  );
}

But there are times where you don’t want to inherit layouts or even the app wrapper, so we also added a way for you to opt-out of that.

export const config: RouteConfig = {
  skipAppWrapper: true, // Disable rendering app wrapper
  skipInheritedLayouts: true, // Disable already inherited _layout templates
};

Thanks to Michael Gearhardt for kicking off the work on this feature!

Async layouts and async app wrapper

Once we had landed support for layouts, we wondered what would happen if we made them the same as a route? Could we allow async layout components too? It would surely reduce mental load by making route components and layout components behave the same. After a bit of coding, it turned out we can. So this Fresh release brings you not just _layout components, but also async layouts!

export default async function Layout(req: Request, ctx: LayoutContext) {
  const person = await fetchSomeData();

  return (
    <div>
      <h1>Hello {person.name}</h1>
      <ctx.Component />
    </div>
  );
}

And while we’re at it, why not make the app wrapper async too? With Fresh 1.4, all layouts behave the same. The only special case are Routes because they’re at the end of the rendering chain and therefore don’t have a ctx.Component property.

Quicker typing with define functions

With the introduction of async route components we received some feedback that the function definition becomes a bit “wordy” with it needing so many keywords.

export default async function Page(req: Request, ctx: RouteContext) {
  // ...
}

And looking at that we share those concerns. I noticed that it always took me a bit of time to type that out. Sure, one could add a custom snippet in your editor to create that boilerplate, but that seemed more of a workaround than a proper solution.

So we spent some time bouncing a few ideas back and forth until we came up with the concept of define* helper functions. They don’t contain any logic, but they provide autocompletion hints to editor out of the box, without having to define the types yourself.

// Both `req` and `ctx` will have the correct type already
export default defineRoute(async (req, ctx) => {
  // ...
}

If you look at the two snippets, there doesn’t seem to be much of a difference. But when you type them out in your editor, you’ll notice that the latter is much quicker to type.

The following define helper functions are available:

  • defineRoute for creating routes
  • defineLayout for creating layouts
  • defineApp for creating the app wrapper.

Organise your code with Route Groups

Normally, nested folders inside the routes/ directory are mapped directly to URLs. However, with bigger projects there are often scenarios where you want to group files and not have that affect the structure of the URL.

Route groups make this possible. A route group is a folder inside routes/ whose name is surrounded by parenthesis like (my-group). This also allows you to have different _layout and _middleware files for routes on the same segment.

routes/
  (marketing)/
    _layout.tsx
    about.tsx  # Maps to /about

  (blog)/
    _layout.tsx
    archive.tsx # Maps to /archive

Colocated islands, components and more

When the name of a route group folder starts with an underscore, like (_components), Fresh will ignore that folder and it’s effectively treated as private. This means you can use these private route folders to store components related to a particular route. The one special name is (_islands) which tells Fresh to treat all files in that folder as an island.

routes/
  shop/
    (_components)/  # ignored by the router
      Section.tsx

    (_islands)/     # local islands folder
      Cart.tsx

    index.tsx

Combined together, this gives you the ability to organise your code on a feature basis and put all related components, islands or anything else into a shared folder.

What’s on the horizon?

There were lots more features in the works, that didn’t make the cut because they need a bit more time to cook. In particular, we’re working on overhauling our plugin system to make it easier to understand and more powerful. The PR to add support for view transitions is coming along nicely and with that we’re exploring how to add spa-like client navigation to Fresh. Another area we’ve been looking at are styling solutions like UnoCSS, using tailwind directly and other solutions.

Like in the past month you can follow this months iteration plan on GitHub.

Did you know? Deno 1.36 was just released.

Be sure to check out the release notes for Deno 1.36, which comes with improved security controls, testing, benchmarking, and more.