From a848193a7aa0b39011380b9d75496b9b49c40f75 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Thu, 10 Sep 2026 00:53:37 +0800 Subject: [PATCH] fix: `emitFile`, eager/priority modes, noscript, srcset --- .changeset/lucky-moons-repeat.md | 17 ++++ README.md | 24 ++++- src/__tests__/browser/solid-image.test.tsx | 48 +++++++++ src/__tests__/components.test.tsx | 85 +++++++++++++++- src/__tests__/vite-plugin.test.ts | 78 ++++++++++++++- src/core/index.tsx | 108 +++++++++++++++------ src/vite/index.ts | 64 ++++++++++-- 7 files changed, 377 insertions(+), 47 deletions(-) create mode 100644 .changeset/lucky-moons-repeat.md diff --git a/.changeset/lucky-moons-repeat.md b/.changeset/lucky-moons-repeat.md new file mode 100644 index 0000000..f1541f7 --- /dev/null +++ b/.changeset/lucky-moons-repeat.md @@ -0,0 +1,17 @@ +--- +"@solidjs/image": minor +--- + +The `img` now carries a `srcset` of the last output format, so a browser that supports none of the `source` formats still picks a sized variant. It used to fall back to the full size original. + +The original image is no longer imported. `src.source` points at the largest variant of the fallback format, so the untouched original never reaches the bundle. + +Processed images go through the bundler on build, so `base`, `assetsDir` and the build manifest now apply to them. The dev server still writes them to the public directory. + +Encoded images are now cached between builds in the Vite cache directory, so a build only encodes images that changed. + +`sizes` now reaches the `img` as well as every `source`, since the `img` carries its own `srcset`. + +`SolidImage` takes an `eager` prop for the image above the fold. It loads right away instead of waiting for the observer, and the server renders it in full so the browser finds it while parsing the page. + +Readers with no JavaScript now get the image. The server renders a `noscript` copy alongside the lazy one. diff --git a/README.md b/README.md index ee0d840..c60245d 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,9 @@ Optimized image components and Vite tooling for [Solid](https://solidjs.com). - `SolidImage` renders a responsive `` that reserves the aspect ratio, so the page does not shift while the image loads. -- The image loads once it scrolls into view. +- The image loads once it scrolls into view. Mark the image above the fold as `eager` and it loads right away. - A tiny preview of the image is inlined in the page and painted behind it, so there is something to look at from the first frame. +- Readers with no JavaScript still get the image. - Your placeholder shows until the image is ready. - The Vite plugin resizes and reformats local images at build time. - Remote images go through your own URL mapping, so a CDN can serve the variants. @@ -159,6 +160,7 @@ The component works on its own. Pass `src` and an optional `transformer`: | `alt` | `string` | yes | Alternative text. | | `fallback` | `(visible: () => boolean, onLoad: () => void) => JSX.Element` | no | Placeholder shown while the image loads. | | `transformer` | `SolidImageTransformer` | no | Produces the responsive variants for `src`. | +| `eager` | `boolean` | no | Loads the image right away instead of waiting for it to scroll into view. | | `sizes` | `string` | no | Value of the `sizes` attribute, such as `50vw`. | | `onLoad` | `() => void` | no | Called once the image has loaded and the placeholder is hidden. | | `crossOrigin` | `JSX.HTMLCrossorigin` | no | Forwarded to the ``. | @@ -180,6 +182,16 @@ Width descriptors do not tell the browser how wide the image will be on the page ``` +### Above the fold + +Lazy loading costs time for the first image on the page, because nothing starts until the observer reports. Mark that one image as `eager`. + +```tsx + +``` + +The server then renders the real image instead of a blank placeholder, so the browser finds it while it parses the page. Leave every other image lazy. + ### Types ```ts @@ -215,6 +227,7 @@ Notes on the shape: - `width` and `height` are the intrinsic pixel size. They only reserve the aspect ratio box, so any pair with the right ratio works. - Variants are grouped by `type`, and each group becomes one `` with a merged `srcset`. - The browser takes the first `` it supports, so order your output formats from most to least preferred. +- The `` carries the last group as its own `srcset`, for a browser that supports none of the formats above it. Make that group the most widely supported format. - Without a transformer no `` is rendered, and the browser loads `src.source`. ### `imagePlugin(options)` @@ -239,10 +252,12 @@ Handles imports ending in `?image`. | `placeholder` | `boolean \| { size?: number }` | `true` | Inline preview of the image. Set a `size` in pixels, or `false` to skip it. | - One file is emitted per output format and per size. `output: ["webp", "jpeg"]` with `sizes: [480, 800]` gives four files per image. -- Files are written to `/.image/i--.`, and the module exports the URL `/.image/i--.`. +- On build the files go through the bundler as assets, so `base`, `assetsDir` and the build manifest apply to them. Nothing is written to `publicPath`. +- On the dev server the files are written to `/.image/i--.` and served from `/.image/...`. - `publicPath` should be served at the root of your site. Add `.image` to `.gitignore` when it sits inside a checked in directory such as `public`. +- The `` falls back to the largest size of the last output format. The original file is never imported, so it does not reach the bundle. - The hash covers the source path, the size and modification time of the source file, the format, the width and the quality. -- A file that already exists is left alone, so images are encoded once and reused on later builds and dev server restarts. +- An image is encoded once and reused. The dev server reuses the file in `publicPath`. A build reuses its copy in the Vite cache directory. - Editing an image or changing an option produces a new name, so a stale file is never served. #### `options.remote` @@ -263,7 +278,8 @@ Handles imports starting with `image:`. 4. Once visible, the `` and your placeholder render. The image starts transparent. 5. Your placeholder calls `onLoad` to say it is on screen. 6. When the image finishes loading after that call, the placeholder is hidden, the image fades in over the preview, and the `onLoad` prop fires. -7. On the server the `` carries a blank SVG of the same size, so nothing is fetched before the image is in view. The placeholder and the loading logic are client only. +7. On the server a lazy `` carries a blank SVG of the same size, so nothing is fetched before the image is in view. An eager `` renders in full. The placeholder and the loading logic are client only. +8. The server also renders a `