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
- Custom html, head and body tags
- Layouts
- Async layouts and async app wrapper
- Quicker typing with define functions
- Organise your code with Route Groups
- What’s on the horizon?
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 routesdefineLayout
for creating layoutsdefineApp
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.