Conversation
|
A couple points coming to my mind:
|
Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
|
For your first 2 points, the graph doesn't change based on these scenarios. Cyclical dependencies might creates unwanted invalidations; you wind up with more pages being rebuilt than you would like, but from Astro's perspective this is the correct thing to do, as any of those modules changing could affect the output so we must treat it as an invalidation. In reality I think this only happens if you have a module that is used by a lot of pages and it changes often. Like if you somehow added an abstraction on top of content collections itself. For dynamic imports, it doesn't matter if you conditionally import based on data. The invalidation is based on the graph, so if a dynamic import changes, that page is invalidated. It doesn't wait to see what happens at runtime. This is an extra reminder to make sure the code includes dynamic imports in the graph though. |
|
About image generation, I wouldn't expect there to be randomness in that process. That sounds like a possible bug, please report if it's something you can recreate. |
|
In review withastro/astro#17084. it was brought up that middleware can effect output. That's true, middleware can literally do anything, it can mutate every response. So from the framework perspective we can't trust any project with middleware. That would be overkill. I think this is likely a documentation thing, but maybe there's another solution. |
|
That's odd, though. i18n is purely middleware; does that mean that projects that use i18n routing can't use this feature? |
This finding and f0236de workaround is a bit of a bummer and I’m confused by the finding, because I think it is mixing two things:
Item 2 (out-of-process prerenderers) is annoying. They would need to have a way to inject-back their content collection entries and that inter-process communication is a bit rabbit hole. I agree that these out-of-process adapters are just at-risk at this stage. However, I am not super familiar with how generate.ts gets multi-threaded when Maybe something like |
|
@ematipico No this doesn't affect i18n. What I mean is that middleware could do: export const onRequest = defineMiddleware((ctx, next) => {
const response = await next();
if(ctx.url.pathname.startsWith('/foo')) {
const html = await response.text();
const newHtml = mutateResponseHtml(html);
return new Response(html, ....)
}
})It can change the response of any page. We only invalidate on cacheKey changes, we don't know about this sort of thing middleware might be doing. |
|
@adamchal That's a workaround for the experimental release, I want to resolve that before stable. We should stop using global state and concurrency will work just fine, but that's a bigger thing to resolve than I had time for for this release. |
|
If the cacheKey has been set to a JSON value, this will not be regenerated when the JSON has been changed. 2 、 Then we'll get a I find that this source code parses the cacheKey to String. I'm not sure if this is a design issue, but the cacheKey can only accept string types? But in the RFC, if data changed will not caught by cacheKey, Wouldn't that defeat the purpose of the design? In this case, I want to check if the data(sections) values of my Collection have changed. If they have, rebuild the collection; otherwise, use the cache. But As matthewp sad "The cacheKey is set by the user and is expected to be unique", I'm not sure if I've misunderstood something. I saw the origin discussion #1096 ,and withastro/astro#16240 (comment) also mention this,
For more, I created an issue withastro/astro#17635 and a PR withastro/astro#17638. |
|
@sgalcheung You want to use |
|
Can you? JSON.stringify(), no cache will be generated. Hello everyone, please take this issue seriously. We are currently unable to communicate. I believe the main problem is the unclear definition of requirements, which has led to greater confusion in its use. |
|
Summarising some feedback from some initial testing of the API. There’s also a bit more on Discord in this feedback thread: https://discord.com/channels/830184174198718474/1536673147364057209 TL;DR — the design being top-level and depending on Feedback
OpportunitiesInstead of a route-level entry in
Here’s an example API for how this might work (naming could be bikeshedded of course). It is aligned with the ---
import { getEntry } from 'astro:content';
import fs from 'node:fs/promises';
// Inputs not tracked by the import graph are loaded inside a special function
export const getStaticData = async (ctx) => {
const entry = await getEntry('pages', ctx.props.id);
const file = await fs.readFile('./some-data.csv', 'utf-8');
return {
cacheKey: ctx.digest([entry.digest, file]),
data: { entry, file }
};
}
const { entry, file } = Astro.staticData;
---Render time behaviour:
Ideally components should know about their props when collecting data to support use like <BlogCard id="foo" />where Some user scenarios
Final notesI hope this doesn’t come off as too critical — obviously this is a big, exciting, and challenging feature! My current evaluation from a user POV is somewhat negative though: I feel there are a lot of somewhat painful tradeoffs required to adopt the API such that you would only use it in extreme cases where it is absolutely essential to reduce build time. If the intention is for it to be an advanced API only used in rare cases, that may be acceptable, but I wish the migration path from not using caching to using caching could be smoother rather than requiring users to re-architect their site data usage. |
|
Allowing build.concurrency > 1 would be a great help as sometimes even changes typically affect around a lot of pages so generating those pages with build.concurrency = 1 takes a considerable amount of time. |
|
There's the other side of the coin, which shouldn't be overlooked. Updating the cache of a component could potentially have a drastic side effect on all its consumers (importers). At a point where many pages, even more than you might think, could get updated. So I would like if there was a comparison against this mental model, because I doubt there's a winner, and we should make sure we find a compromise. |
I’m not sure I understand the concern — that seems desirable behaviour to me? If a component changed, the page should be updated. Not re-rendering a page that contains components that changed is a bug surely? |
|
There are two other potential performance gotchas I've come across with the Cloudflare adapter. 1. Base64 / JSON stringifying of the prerendered body
const envelope: PrerenderEnvelope = {
status: response.status,
statusText: response.statusText,
headers: [...response.headers.entries()],
body: arrayBufferToBase64(bufferedBody),
metadata: { contentEntryKeys, staticImages },
};
return new Response(JSON.stringify(envelope), {
headers: { 'Content-Type': 'application/json' },
});This can be punishing, especially if your pages are large. 2.
|
Using fs.stat to retrieve metadata might be better. Regarding the handling of cacheKeys, I agree with delucis's opinion; the PR: Fix cachekey in incremental could have done even better.
|
Components that come from libraries. If they change due to lock file change, there might be changes that users don't want. I am not saying that it's not desirable, and not even that your proposal is sound, but I see possible drawbacks too. |
|
@adamchal yeah I don't love that either, have been thinking about it. HTTP is internal to how Cloudflare communicates, the Astro prerenderer API doesn't prescribe that. So perhaps internally the Cloudflare adapter can use multiple requests for a build instead of a single one, to bring back the other parts. |
|
@matthewp yeah, my thinking was: Are you trying to bite off the concurrency issue too? I haven’t dug in how deep that would cut, but I’m sure this IPC would be a side-effect or directly affected. @sgalcheung great call out to use a the cheaper |
|
@delucis Outside of whether we should do it or not, from an implementation standpoint, |
|
I have a branch with support for concurrency. I'll take the perf issues into consideration there. |
Very likely that I’m missing some technical details of the render flow, but my thinking was that you’d use it the same way we use the render export currently (pseudo-code): import Component, { getStaticData } from 'compiled-user-component';
const data = getStaticData();
Component({ result, props, slots, data });with internal compiler stuff looking like: const $$Index = $$createComponent(($$result, $$props, $$slots, $$data) => {
const Astro = $$result.createAstro($$props, $$slots, $$data);
Astro.self = $$Index;
const { myData } = Astro.data;
return $$render`${$$maybeRenderHead($$result)}<div>${myData}</div>`;
}, "example.astro", undefined);I’d think everything is already in the graph so no change in performance? And certainly not worse than running data manipulation for all pages for each render in dev. Dev also doesn’t need any cache handling I’d assume? So |
|
@adamchal I've addressed the encoding issue you raised in this PR, along with enabling concurrency: withastro/astro#17795 |
|
@matthewp ran a test of this against two real live repos—the performance and concurrency seem really good, but I haven’t studied the implementation. |
|
I'm trying this out on a project and I'm coming across an issue that it's processing each page every build even if nothing's actually changed. Claude seems to think it might be the astro-icon package - generating unique things every build but I'm not sure. I've commented out any instances of and run two builds back to back and Claude seems to think that that's the issue. `So to directly answer "why are they being listed again" — it's not that your pages are getting rebuilt unnecessarily because of something wrong in what we built. It's that Astro's dependencyHash check (the other half of canSkip(), checked before cacheKey even gets compared) is coming out different on every single build, even when the source is byte-identical. Per Astro's own source, that hash is built from the actual bundled/transformed code of every module in the page's render graph — Layout, every component it pulls in, their CSS — not from your files specifically. Something in that pipeline is producing non-deterministic output build-to-build. The most likely suspect given the log is astro-icon's Loaded icons from src/icons step, which re-scans and rebuilds the icon registry fresh every run — if that doesn't produce byte-identical output each time (e.g. non-deterministic ordering), it alone would poison the hash for every page that uses an icon. This is a real limitation, but it sits one layer below anything we've touched — incrementalBuild is still explicitly an experimental Astro feature (that's literally in the comment we already have in astro.config.mjs), and we've now proven the CMS-timestamp cacheKey side of this is solid and stable. The dependencyHash instability is Astro/Vite/astro-icon's own build determinism, not something staticPaths.js or cacheKey.js can influence. There is a way to actually pin down which specific module is unstable — I'd temporarily patch Astro's own hashModules() (in node_modules/astro/dist/core/build/plugins/plugin-incremental.js) to log a hash per module, run two builds back to back, and diff which module id changed. That's real node_modules surgery for a one-off diagnosis (reverts on your next npm install), and it's a genuine rabbit hole with no guarantee the culprit is fixable from userland even once found.` `Confirmed — this nails it down completely. The cached output copies in node_modules/.astro/dist/case-studies/.html are still timestamped from Build 1 (15:12:5x), untouched by Build 2. The live dist/case-studies/.html files did update at Build 2's time (15:14:26), but that's just Astro's restoreOutputFile() copying the cached HTML across — not a fresh render. No CMS fetch happened for these 5 pages in Build 2. The skip actually worked. So: astro-icon was indeed the source of the dependencyHash instability. With disabled, two back-to-back builds now produce identical dependency hashes and Astro correctly reuses the previous build's output. Obviously we can't ship the site with every icon commented out — that was purely diagnostic. The real fix is figuring out what about astro-icon's output isn't deterministic. Let me check its config/version to see if there's a known cause: Used 14 tools Found the actual root cause, and it's outside our codebase entirely. Tracing further into @iconify/tools (a dependency of astro-icon), its scanDirectory() function (node_modules/@iconify/tools/lib/misc/scan.mjs) reads your icons folder with plain fs.promises.readdir() and pushes results into an array with no sort applied:` |
|
We should have something to let the user know why a page is a cache miss. Will have to think of a way to show this without it becoming too much information. |
|
We have some kind of internal framework based on Astro at Seibert and we would need this because it's used for dozens of sites with thousands of mdx pages. But we face the same limitation as Starlight where stuff could happen anywhere in the tree, so it's unfeasible to just set a cacheKey of some kind at the top |

Summary
Incremental static builds, allowing Astro to skip regenerating prerendered pages whose template dependencies and per-path data are unchanged since the previous build.
Links