Skip to content

Add opt-in Cloudflare binding image optimization during build - #16194

Merged
alexanderniebuhr merged 4 commits into
withastro:mainfrom
Daedalus-Icarus:fix/issue-16035
Aug 2, 2026
Merged

Add opt-in Cloudflare binding image optimization during build#16194
alexanderniebuhr merged 4 commits into
withastro:mainfrom
Daedalus-Icarus:fix/issue-16035

Conversation

@Daedalus-Icarus

@Daedalus-Icarus Daedalus-Icarus commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Changes

  • Adds an opt-in compound imageService configuration for using the Cloudflare Images binding during build-time prerendering. The default imageService: 'cloudflare-binding' shorthand keeps the existing runtime-only behavior.
  • Uses the Cloudflare IMAGES binding in the workerd prerender worker to transform static images, writes successful transforms to the client output, and falls back to Sharp only when a transform is not returned by the binding.

Testing

  • Adds coverage for the config normalization behavior so the shorthand remains runtime-only and the compound cloudflare-binding config opts into build-time transforms.
  • Keeps the existing Cloudflare image-service coverage for binding, compile, external, dev endpoint, and static output behavior.

Docs

@changeset-bot

changeset-bot Bot commented Apr 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e84ed87

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 40 packages
Name Type
@astrojs/cloudflare Minor
@test/astro-cloudflare-node-prerender-mdx Patch
@test/astro-cloudflare Patch
@test/astro-cloudflare-allowed-hosts Patch
@test/astro-cloudflare-astro-dev-platform Patch
@test/astro-cloudflare-astro-env Patch
@test/astro-cloudflare-binding-image-service Patch
@test/astro-cloudflare-cache-provider-wait-until Patch
@test/astro-cloudflare-cache-provider Patch
@test/astro-cloudflare-client-address Patch
@test/astro-cloudflare-compile-custom-image-service Patch
@test/astro-cloudflare-compile-image-service Patch
@test/astro-cloudflare-custom-entryfile Patch
@test/astro-cloudflare-dev-image-endpoint Patch
@test/astro-cloudflare-external-image-service Patch
@test/astro-cloudflare-external-redirects Patch
@test/astro-cloudflare-internal-redirects Patch
@test/astro-cloudflare-no-output Patch
@test/astro-cloudflare-prerender-node-env Patch
@test/astro-cloudflare-prerender-queue-consumers Patch
@test/astro-cloudflare-prerender-styles Patch
@test/astro-cloudflare-prerenderer-errors Patch
@test/astro-cloudflare-prerenderer-render-error Patch
@test/routing-priority-cloudflare Patch
@test/cf-server-entry Patch
@test/astro-cloudflare-server-island-prerender-framework Patch
@test/astro-cloudflare-sessions Patch
@test/astro-cloudflare-sql-import Patch
@test/cf-ssr-deps Patch
@test/astro-cloudflare-static Patch
@test/astro-cloudflare-svelte-rune-deps Patch
@test/astro-cloudflare-top-level-return Patch
@test/cf-user-optimize-deps Patch
@test/astro-cloudflare-vite-plugin Patch
@test/astro-cloudflare-with-base Patch
@test/astro-cloudflare-with-react Patch
@test/astro-cloudflare-with-solid-js Patch
@test/astro-cloudflare-with-svelte Patch
@test/astro-cloudflare-with-vue Patch
@test/astro-cloudflare-wrangler-preview-platform Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@alexanderniebuhr alexanderniebuhr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is correct. In theory the image binding should have sharp for prerender optimization. And the binding should be available when workerd is used for prerendering.

cc @OliverSpeir @Princesseuh who might have more context to share

@OliverSpeir

Copy link
Copy Markdown
Contributor

Cloudflare-binding intentionally does not transform images on pre-rendered routes locally during build. The intention is they go to the image endpoint at request time as well, one benefit of this is faster build times

@alexanderniebuhr

alexanderniebuhr commented Apr 2, 2026

Copy link
Copy Markdown
Member

It should use the cloudflare binding itself to optimize them during build time though, not sharp. So like during workerd prerendering the image endpoint should be requested, which then uses cloudflare's own binding to optimize the images.

@alexanderniebuhr

Copy link
Copy Markdown
Member

@Desel72 I'm sorry, but we are still discussing if this is actually an bug or the expected behavior on Discord. So we might end up not merging this, but no decision yet.

@Daedalus-Icarus

Copy link
Copy Markdown
Contributor Author

Thank you for the context, @alexanderniebuhr! I think I misunderstood the design intent behind cloudflare-binding. If the decision is that build-time optimization should happen for prerendered routes, I think the correct approach would be to have the workerd prerender worker call the image transform endpoint (which uses the IMAGES binding) during the build phase, rather than falling back to Sharp on the Node side. Happy to rework the approach if it turns out to be a bug that needs fixing. I think this should be fixed.

@alexanderniebuhr

Copy link
Copy Markdown
Member

If the decision is that build-time optimization should happen for prerendered routes, I think the correct approach would be to have the workerd prerender worker call the image transform endpoint

@Desel72 yeah after discussion we think this the the better way. I'll update the original issue so it makes more sense. Would you like to still work on that? If so, do you want to use this PR or should we close it and you can create a new one?

@Daedalus-Icarus

Copy link
Copy Markdown
Contributor Author

I'd love to use this PR @alexanderniebuhr I will solve perfectly.

@Daedalus-Icarus

Daedalus-Icarus commented Apr 6, 2026

Copy link
Copy Markdown
Contributor Author

Hi @matthewp @alexanderniebuhr I've done. Welcome to your feedback. Thanks

@alexanderniebuhr

Copy link
Copy Markdown
Member

@Desel72 we are going to review the PR as soon as we have bandwidth, but please fix the failing lint check :)

@Daedalus-Icarus

Daedalus-Icarus commented Apr 8, 2026

Copy link
Copy Markdown
Contributor Author

@alexanderniebuhr Thanks for your reviewing. I've solved.

@Daedalus-Icarus

Copy link
Copy Markdown
Contributor Author

Hi @alexanderniebuhr is there any update on this PR?

@alexanderniebuhr

Copy link
Copy Markdown
Member

We are still reviewing this. Current state is that this should be put behind a option, so users can opt-in and opt-out and the changeset can't be a patch, since it's a breaking change.

@alexanderniebuhr alexanderniebuhr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure that this is a major change, which means we need to update the changeset as well as have an docs PR. The behavior should also put behind a flag so users can opt-in or opt-out. I suggest using the triplet configuration suggested here: #15662

@Daedalus-Icarus

Copy link
Copy Markdown
Contributor Author

Please make sure that this is a major change, which means we need to update the changeset as well as have an docs PR. The behavior should also put behind a flag so users can opt-in or opt-out. I suggest using the triplet configuration suggested here: #15662

Hi @alexanderniebuhr, how are you? Sorry for the late. I've solved this. Please review this.

@Daedalus-Icarus

Daedalus-Icarus commented Apr 19, 2026

Copy link
Copy Markdown
Contributor Author

@alexanderniebuhr
For the docs PR, two options — which do you prefer?
-I open it now at withastro/docs
-I wait until the config shape is finalized here
Also let me know if the triplet should include dev too (full { build, dev, runtime } from #15662).

@Daedalus-Icarus Daedalus-Icarus changed the title fix(cloudflare): enable Sharp image optimization for default cloudflare-binding mode Add opt-in Cloudflare binding image optimization during build May 7, 2026
@Daedalus-Icarus

Copy link
Copy Markdown
Contributor Author

Hi @alexanderniebuhr, I addressed the requested changes from the Apr 16 review:

  • The behavior is now opt-in via compound config.
  • imageService: 'cloudflare-binding' keeps the existing runtime-only behavior.
  • The changeset is now a major change.
  • Added test coverage for the config behavior.
  • Updated the PR title/body to match the current behavior.
  • Opened the docs PR: Document Cloudflare binding build-time image optimization docs#13848

Local verification:

  • pnpm -C packages/integrations/cloudflare build
  • pnpm -C packages/integrations/cloudflare exec astro-scripts test "test/{binding-image-service,compile-image-service,external-image-service,dev-image-endpoint,static}.test.ts"
  • pnpm -C packages/integrations/cloudflare exec astro-scripts test "test/image-config.test.ts"

Could you please re-review when you have bandwidth?

@Daedalus-Icarus

Copy link
Copy Markdown
Contributor Author

@alexanderniebuhr
Could you please review my PR?
Thanks!

…ding

Adds a compound imageService config { build: 'cloudflare-binding', runtime?: ... }
that opts in to transforming static images with the Cloudflare IMAGES binding in
the workerd prerender environment at build time, writing the optimized bytes
directly to the output directory (falling back to Sharp when the binding fails).
The string shorthand 'cloudflare-binding' keeps runtime-only behavior.

Closes withastro#16035
Co-authored-by: Daedalus <6442298+Daedalus-Icarus@users.noreply.github.com>

@matthewp matthewp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is now covered, you can provide an object to differentiate between build time and runtime image services: https://docs.astro.build/en/guides/integrations-guide/cloudflare/#imageservice

@adamchal

Copy link
Copy Markdown
Contributor

@matthewp this gives the ability to use Cloudflare IMAGES binding to generate images during build. Right now, the only options that can generate images during build are compile and custom. And if you use a custom image.service you do have a lot of flexibility—doing virtually whatever you want during build and runtime—except use the Cloudflare IMAGES binding during build.

@adamchal adamchal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Daedalus-Icarus I love this idea, but I have some scaling concerns.

hash,
finalPath,
transform: transform as Record<string, any>,
imageData,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This base64 and JSON encodes every prerendered transform into a single response. All optimized variants (+~33% for base64) are held in memory in full twice: once on the workerd side to JSON.stringify, once on the Node side to parse.

The runtime image-transform-endpoint avoids this by streaming raw bytes one image at a time. I think the build path should adopt a similar shape. It would be more local HTTP requests, but would allow for scale.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamchal you are right, we can optimize here. Let me push an updated approach.

@alexanderniebuhr

Copy link
Copy Markdown
Member

I think this is now covered, you can provide an object to differentiate between build time and runtime image services: docs.astro.build/en/guides/integrations-guide/cloudflare#imageservice

I don't think the idea of this PR is covered. Let's discuss this https://discord.com/channels/830184174198718474/845430950191038464/1530469987502915615

…ng them

The build path base64-encoded every prerendered transform into a single JSON
response. Peak memory held the whole optimized image set several times over:
once as base64 in the entries array, once in the `JSON.stringify` result, once
as the encoded response body, then again on the Node side to parse. A site with
a few hundred variants could exceed the isolate memory limit, and a large enough
one hits V8's max string length in `JSON.stringify`.

Transform one image per request instead, mirroring the runtime
`image-transform-endpoint`: `/__astro_image_transform` streams the optimized
bytes straight from the IMAGES binding into the client output directory, with
bounded concurrency. Peak memory is now proportional to the concurrency limit
rather than to the image set.

This also fixes the binding never actually running. The worker resolved the
original through the ASSETS binding, but at that point in the build the
unoptimized original lives in Astro's prerender output, not in the client
directory ASSETS serves, so every transform failed with an empty input. The
failure was swallowed by a bare `catch {}` and silently fell back to Sharp, so
builds looked successful while the binding did nothing. The original is now
streamed up as the request body, and failures are logged instead of hidden.

Adds a test that builds with the binding config and fails if any image falls
back to the Node-side service.
@matthewp
matthewp dismissed their stale review July 25, 2026 11:46

Not what I thought this was.

@ArmandPhilippot ArmandPhilippot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a nit for the changeset, otherwise docs LGTM on this side. Thanks.


I don't want to block the PR, but here is my opinion on the DX, from the perspective of someone who might be using the Cloudflare integration for the first time.

I was reviewing the docs PR and, although my lack of familiarity with Cloudflare might not be helping, I think the current behavior for the shorthand is confusing.

The object shape is fine and, yes, this sounds like an improvement for users! But, IMO, imageService: 'cloudflare-binding' doesn't meet the POLA.

IIUC:

  • with imageService: 'cloudflare-binding', 'cloudflare-binding' is only used at runtime (and falls back to something else at build-time... I don't think we explain that in docs)
  • with imageService: { build: 'cloudflare-binding' }, 'cloudflare-binding' is used both at build-time and runtime
  • with imageService: { build: 'compile', runtime: 'cloudflare-binding' }, 'cloudflare-binding' is runtime only

In this context, I would expect imageService: 'cloudflare-binding' to be syntactic sugar for imageService: { build: 'cloudflare-binding', runtime: 'cloudflare-binding' }. This is not the case and someone would have to write the long version to use cloudflare-binding everywhere.
(well, it seems they can use imageService: { build: 'cloudflare-binding') }, right, but this doesn't change my point about what the shorthand should do).

I understand that this would be a breaking change for the integration as we can't just swap the default to use an object matching the current behavior (e.g. { build: 'compile', runtime: 'cloudflare-binding' }). Someone could have set imageService: 'cloudflare-binding' explicitly.

But, shouldn't we choose to release a major in order to improve the DX here? (and so rework the current way the shorthand works, now that users can use an object to fine-tune the configuration)

Comment thread .changeset/fix-cloudflare-binding-image-optimization.md Outdated
@alexanderniebuhr

Copy link
Copy Markdown
Member

@ArmandPhilippot I agree with you, but IIRC we decided against changing the default for now, so this stays a minor, and we can do the switch later if wanted in a major. I also think your summary is not quite right, but I have to validate that.

setting build with binding runtime with binding
imageService: 'cloudflare-binding' no yes
imageService: { build: 'cloudflare-binding' } yes no
imageService: { runtime: 'cloudflare-binding' } no yes
imageService: { build: 'compile', runtime: 'cloudflare-binding' } yes yes

Co-authored-by: Armand Philippot <git@armand.philippot.eu>
@ArmandPhilippot

Copy link
Copy Markdown
Member

@alexanderniebuhr Yeah, I understand. As I said, I won't block the PR, but I thought I had to express my concern because it's not easy to document, and so I expect it to be just as confusing for users...

And speaking of confusion... 😄 You said yourself in the docs PR that "runtime: 'cloudflare-binding' is the default when build is 'cloudflare-binding'" (which seems right), and build is always required. So, I think your table should rather be:

setting build with binding runtime with binding
imageService: 'cloudflare-binding' no yes
imageService: { build: 'cloudflare-binding' } yes yes
imageService: { runtime: 'cloudflare-binding' } impossible impossible
imageService: { build: 'compile', runtime: 'cloudflare-binding' } yes yes

@alexanderniebuhr

Copy link
Copy Markdown
Member

I think we should go ahead with this one, and do any other change in follow ups.

@alexanderniebuhr
alexanderniebuhr merged commit 2a59663 into withastro:main Aug 2, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: integration Related to any renderer integration (scope)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants