Astro 7.4 is here! The headline feature is a new --timings flag for astro build that shows exactly where your build spends its time. This release also lets you use different image services for prerendered and on-demand pages, adds a notFound() handler for custom entrypoints, and warns you about font settings your provider can’t serve. Two new experimental features round it out: parallel prerendering across worker threads and a custom redirect page for static builds.
Explore what’s new in this release:
- Build timings
- Separate build and runtime image services
- notFound() handler for custom entrypoints
- Font configuration warnings
- Updated minimum Node.js version
- Experimental: parallel prerendering
- Experimental: custom redirect page
- Community
To upgrade an existing project, use the automated @astrojs/upgrade CLI tool. Alternatively, upgrade manually by running the upgrade command for your package manager:
# Recommended:npx @astrojs/upgrade
# Manual:npm install astro@latestpnpm upgrade astro --latestyarn upgrade astro --latestBuild timings
When a build gets slow, it’s not always obvious why. Is it a Vite plugin, syntax highlighting, image processing, or rendering one particularly heavy page?
astro build now accepts a --timings flag:
astro build --timingsWhen the build finishes, Astro prints a summary of where the time went: build phases, Vite plugins, Markdown processing and syntax highlighting, page rendering, images, and integrations.
Here’s what that looks like in practice. We took the blog starter and added a <CommentSection> component to its blog post layout. It fetches a post’s comments, and each <Comment> inside it then fetches the commenter’s profile:
---const { comment } = Astro.props;// Look up the commenter's profile for every commentconst res = await fetch(`https://jsonplaceholder.typicode.com/users?email=${comment.email}`);const [user] = await res.json();---
<article> <strong>{user?.name ?? comment.name}</strong> <p>{comment.body}</p></article>Nothing about this looks wrong in the editor, but the profile requests can’t start until the comments come back, so every page waits on two network round trips in a row. Running astro build --timings makes that easy to see (some sections trimmed):
build timingstotal 2.61s · 9 pages · output "static"
Build phases resolve config 59ms 2.3% ▄····················· setup 225ms 8.6% ▃█▅··················· route discovery 2ms 0.1% ▁····················· vite config 14ms 0.5% ▁····················· content sync 206ms 7.9% ▂█▅··················· prerender bundle 151ms 5.8% ··▃█·················· client bundle 21ms 0.8% ····▂················· manifest injection 2ms 0.1% ····▁················· generate pages 2.10s 80.6% ····▇█████████████████ prerenderer setup 6ms 0.2% ····▁················· static paths 3ms 0.1% ····▁·················
Page rendering (2.09s across 9 pages) component render 2.08s 79.6% ····▆█████████████████ page pipeline 13ms 0.5%
Slowest pages (median 297ms · p95 571ms) /about 571ms /blog/first-post 307ms /blog/[...slug] /blog/using-mdx 303ms /blog/[...slug] /blog/second-post 299ms /blog/[...slug] /blog/third-post 297ms /blog/[...slug] /blog/markdown-style-guide 296ms /blog/[...slug] /rss.xml 10ms /blog 6msThe report tells the story from the top down. Generating pages takes 81% of the build, and nearly all of that is spent inside components rather than in Astro’s own page pipeline. The slowest pages are the ones that use the blog post layout, while /blog and /rss.xml render in milliseconds. That points straight at something the layout renders.
Fetching the comments and all the profiles at once with Promise.all() in <CommentSection>, and passing each profile down to <Comment>, cut page generation from 2.10s to 1.13s and halved the time for each blog post.
The report also covers Vite plugins, the slowest files to bundle, Markdown plugins, syntax highlighting, and image transforms, so the same flag helps whether your slow build comes from rendering, bundling, or images. It’s a good first step before trying flags like parallel prerendering or incremental builds, so you know which part of the build to target. See the --timings reference for more.
Separate build and runtime image services
Prerendered pages and on-demand pages have different needs when it comes to images. At build time you have plenty of time and a full Node.js environment, so a local optimizer like Sharp works well. On-demand pages run on your server or at the edge, often in a runtime where Sharp can’t run, so an image CDN is usually the better fit. Until now you had to pick one image service for both, or rely on your adapter to make the choice for you.
In Astro 7.4, image.service also accepts a build and a runtime service:
import { defineConfig } from 'astro/config';
export default defineConfig({ image: { service: { build: { entrypoint: 'astro/assets/services/sharp' }, runtime: { entrypoint: 'my-image-cdn-service' }, }, },});The build service handles images on prerendered pages, generates them during the build, and is also used during development. The runtime service handles on-demand pages and the image endpoint in production. Passing a single service still works the same as before.
To make this possible, we reworked how Astro tracks the images used during a build. Prerenderers now take part in Astro’s own image pipeline instead of having to recreate it themselves. As a result, @astrojs/cloudflare now processes images on prerendered pages through the same pipeline as every other adapter.
notFound() handler for custom entrypoints
The composable handlers in astro/fetch and astro/hono let you build your own server or worker entrypoint from Astro’s pieces. However, if you used pages() without the all-in-one astro() handler, a request that didn’t match any route returned an empty 404 instead of your custom 404.astro page, even if it was prerendered.
Astro 7.4 adds a notFound() handler. Add it before pages() to render your 404 page when no route matches:
import { Hono } from 'hono';import { notFound, pages } from 'astro/hono';import { cf } from '@astrojs/cloudflare/hono';
const app = new Hono();
app.use(cf());app.use(notFound());app.use(pages());
export default app;The same handler is available from astro/fetch. astro() already includes this behavior, so you only need notFound() when you compose the handlers yourself. See the notFound() reference for details.
Font configuration warnings
It’s easy to ask for a font weight or subset that a font family doesn’t offer, and until now you’d only find out when the text looked wrong. Font providers can now implement an optional getFontProperties() callback that reports the weights, styles, subsets, and formats a family actually has, and Astro warns you about any values in your config that the provider can’t serve.
All of Astro’s built-in font providers support this. If you maintain a custom font provider, we recommend implementing getFontProperties() now. It’s optional in Astro 7, but will be required in Astro 8:
import type { FontProvider } from 'astro';
const provider: FontProvider = { name: 'my-provider', resolveFont: ({ familyName }) => { // ... }, getFontProperties: ({ familyName }) => ({ weights: ['400', '700'], styles: ['normal', 'italic'], subsets: ['latin'], formats: ['woff2'], }),};The init() context for font providers also gains a fetch() function that retries transient failures. Providers that wrap a third-party unifont provider can pass the context straight through to it. See the font provider reference for details.
Updated minimum Node.js version
The minimum supported Node.js version for Astro and its official integrations is now 22.22.0. This is the latest Node.js 22 release supported by StackBlitz, and the change follows Astro’s Node.js support policy. If you’re on an older Node.js 22 release, upgrade Node.js before upgrading Astro. See the installation prerequisites for the full list of requirements.
Experimental: parallel prerendering
Astro renders static pages concurrently, but until now all of that rendering happened on a single thread. On large sites with CPU-heavy pages, the build was limited to one core no matter how many your machine had.
Astro 7.4 adds the experimental.parallelPrerender flag, which renders static pages in Node.js worker threads:
import { defineConfig } from 'astro/config';
export default defineConfig({ experimental: { parallelPrerender: true, },});By default, Astro uses all available CPU cores but one. Each worker adds to the memory your build uses, so if your build runs out of memory or doesn’t get faster, set workers to a lower number:
import { defineConfig } from 'astro/config';
export default defineConfig({ experimental: { parallelPrerender: { workers: 4, }, },});Each worker renders as many pages at once as your build.concurrency setting allows.
The flag has no effect when your adapter provides its own way of prerendering pages. Among official adapters, that’s only @astrojs/cloudflare, and Astro logs a warning during the build when the flag is ignored.
This pairs well with incremental static builds: incremental builds skip pages that haven’t changed, and parallel prerendering spreads the pages that do need rendering across your CPU cores. See the experimental parallel prerendering docs for more.
Experimental: custom redirect page
A static site has no server to send an HTTP Location header, so for every redirect it builds, Astro generates a small HTML page with a <meta http-equiv="refresh"> tag. That includes your redirects config, Astro.redirect() calls in prerendered pages, and i18n redirects. That page’s markup, wording, and delay were fixed, so you couldn’t brand or translate it.
The new experimental.redirectPage flag lets you provide your own:
import { defineConfig } from 'astro/config';
export default defineConfig({ experimental: { redirectPage: true, },});Then create src/pages/3xx.astro. Astro renders it once for each redirect, passing that redirect’s from, to, status, and delay as props:
---import type { RedirectPageProps as Props } from 'astro';
const { from, to, status, delay } = Astro.props;---
<html lang="en"> <head> <meta http-equiv="refresh" content={`${delay};url=${to}`} /> <meta name="robots" content="noindex" /> <title>Redirecting…</title> </head> <body> <p>This page has moved. <a href={to}>Continue to {to}</a>.</p> </body></html>In a static build, the <meta http-equiv="refresh"> tag is the redirect, so your page must render one that points to to. Astro logs a warning during the build if it’s missing, and falls back to the built-in page if yours fails to render. Redirects from on-demand pages are unchanged.
See the experimental redirect page docs for more, and leave feedback on the RFC.
Other improvements
- Integrations can control build output directories — Astro now treats Vite’s resolved
outDirfor each environment as the source of truth for where client, server, and prerender output is written, so integrations and Vite plugins can move it. Custom prerenderers can read the final locations from a newoutputDirectoriescontext insetPrerenderer(), and Vite’s build manifests now stay around until everybuildApphook has finished, so deployment integrations can read them. (#18313)
For a complete list of bug fixes and smaller improvements, see the full changelog.
Community
The Astro core team is:
Alexander Niebuhr, Armand Philippot, Chris Swithinbank, Emanuele Stoppa, Erika, Florian Lefebvre, Fred Schott, HiDeoo, Luiz Ferraz, Matt Kane, Matthew Phillips, Reuben Tier, Sarah Rainsberger, and Yan Thomas.
Special thanks to everyone who contributed to Astro 7.4 with code, docs, reviews, and testing, including:
Adam Chalemian, Adam Matthiesen, Alex Hoyau, Alisson Nunes, Anastasia Y., Andreas Deininger, Andrei Alba, Angel Marfil, Asqar Bäkir, Aurora, Austen Stone, Ben Abulafia, breken, bulebrainbrand, Chowdhury Zaber Bin Zahid, Dan Barclay, Daniel Mueller, Danillo Estrela, David Umoru, Edmund Hung, Felix Schneider, fkatsuhiro, Fuan200, George Zhang, Harjot Singh, is*hosting, Jack Hanford, jacobortiz, James Opstad, Jassine, joel hansson, Junseong Park, kdelay, Kevin Deng, KTrain, Leif Marcus, Liang Xu, Louis Escher, Luca Agosta, Lukas, Madan Kumar, Maninderpreet Singh, Manuel, Mauro Daniel Viveros, Michael Wang, Mikhail Alabugin, Ming-jun Lu, Minho Yoo, mixto, mrchatam, Neil de Carteret, ocavue, paul valladares, Pedro Spengler, Peeranat Danaidusadeekul, Piyush Mehta, Pruthvi Shetty, randomguy-2650, Roman, Sakku 🦉, Sapan Mozammel, Shinya Fujino, Shoichi Sasaki, surefirestudios, Tobias Breit, Tomoki Yamauchi, ump45nose, Wataru Sato, Waxer59, Yhvr, and Zhiqi Zhang
We hope you enjoy Astro 7.4. If you run into issues or want to share feedback, please join us on Discord, post on GitHub, or reach out on Bluesky, Twitter, and Mastodon.


