Closed sergiodxa closed 2 years ago
I tried to use an image loader based on query params in imports with vite.js and it was a pain because this kind of imports is impossible to type in typescript. So not sure how I feel about them. But it is definitely a problem worth solving :+1:
@sergiodxa yeah that was the plan when we had the image importer. Always thinking on the same wavelength!
We're not sure that the compiler is the right place to process something as expensive as images so we decided to remove it and put it on the backburner until v1 is humming along. We have some rough ideas about how to help out with images w/o making the compiler do it. I hope we can get to sooner than later!
I'm not sure how Next.js did it but when you import an image file the object you import has a specific type
interface StaticImageData {
src: string
height: number
width: number
blurDataURL?: string
}
I saw they have this:
declare module '*.png' {
const content: StaticImageData
export default content
}
In a global.d.ts
file, not sure if that's the only thing needed to correctly type the import of an image file
Yes declare module '*.png'
is what does the trick. But if you start adding query params to the import path, it doesn’t work any more. You would have to explicitly type every possible query params combination which is obviously not possible.
Next.js also has a way to handle dynamic images with some custom loader without affecting the build time
the way they handle it in run time is they use /_next/image?url=original_url&w=750&q=75
to generate images only when needed
Someone created this gist for a possible way to build it. it's just a POC https://gist.github.com/olikami/236e3c57ca73d145984ec6c127416340
Or maybe as initial implementation you could provide an integration with popular services such as cloudinary or imgix
Also extremely interested by this feature. Gatsby and Next JS provide out of the box.
Is it a way to do it in React without meta framework like Gatsby and Next JS?
Really need image optimizations!
Jacob from the team created a POC of the component using a resource route to do the optimization https://gist.github.com/jacob-ebey/3a37a86307de9ef22f47aae2e593b56f
@sergiodxa I just released the package remix-image
which can automatically create responsive images using resource routes.
https://www.npmjs.com/package/remix-image https://github.com/Josh-McFarlin/remix-image#readme
Still in trial, and many more features need to be added to meet the functionality of next/image
, so feel free to help with any contributions! Right now it only add srcSet
and sizes
to the img
element, but I plan on expanding the library to include support for layouts soon.
Here's my 50 cents. I optimize everything static with Squoosh tailored to the size I want to display it. My main issue was dealing with images that come from the CMS. Clients should not need to worry about performance.
I've taken @jacob-ebey 's example and stripped it down to only serve external images with a predefined width set. For caching, I rely on the CDN instead of caching in the file system. Gonna use this until remix lands its official solution.
// entry.server.tsx
import * as serverSharp from "sharp";
export const sharp = serverSharp.default;
// routes/images.tsx
import React, { ComponentPropsWithoutRef } from "react";
import { LoaderFunction } from "remix";
import { sharp } from "~/entry.server";
const BadImageResponse = () => {
const buffer = Buffer.from(
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
"base64"
);
return new Response(buffer, {
status: 500,
headers: {
"Cache-Control": "max-age=0",
"Content-Type": "image/gif;base64",
"Content-Length": buffer.length.toFixed(0),
},
});
};
export const loader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);
const src = url.searchParams.get("src");
const width = url.searchParams.get("w");
const quality = url.searchParams.get("q");
if (!src || !width || !quality) {
return BadImageResponse();
}
try {
const image = await fetch(src);
if (!image.ok || !image.body) {
throw new Error(
`fetching image failed. src: ${src}, status: ${image.status}`
);
}
const imageBuffer = Buffer.from(await image.arrayBuffer());
let resizedImage = await sharp(imageBuffer)
.resize(parseInt(width))
.webp({
quality: parseInt(quality),
})
.toBuffer();
return new Response(resizedImage, {
status: 200,
headers: {
"Cache-Control": `max-age=${60 * 60 * 24 * 365}, public`,
"Content-Type": "image/webp",
"Content-Length": resizedImage.length.toFixed(0),
},
});
} catch (error) {
console.error(error);
return BadImageResponse();
}
};
const widths = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
const quality = 75;
export const Img: React.FC<ComponentPropsWithoutRef<"img">> = (props) => {
if (!props.src) {
throw new Error("no src provided to Img component");
}
if (props.srcSet) {
console.warn("srcSet will be overwritten by srcSetGenerator");
}
const srcSetParts = widths.map(
(width) => `/images?src=${props.src}&w=${width}&q=${quality} ${width}w`
);
return (
<img {...props} srcSet={srcSetParts.join(", ")} src={srcSetParts[0]} />
);
};
I think this is also a very basic & understandable approach, which others can use and extend to fit their needs, e.g. providing values for width, height and sizes via props. For me, this fire and forget img
wrapper is perfect.
@sergiodxa Next’s Image
has also the priority
prop. It adds link rel=preload
to head
to improve LCP.
@sergiodxa Next’s
Image
has also thepriority
prop. It addslink rel=preload
tohead
to improve LCP.
Because there’s no Head component in Remix that will not be possible I imagine, if you want to preload the image you can add it to the LinksFunction of the route.
Because there’s no Head component in Remix that will not be possible I imagine, if you want to preload the image you can add it to the LinksFunction of the route.
How does the <Link prefetch="render" />
stuff work then? Couldn't we do something similar?
@sergiodxa Next’s
Image
has also thepriority
prop. It addslink rel=preload
tohead
to improve LCP.Because there’s no Head component in Remix that will not be possible I imagine, if you want to preload the image you can add it to the LinksFunction of the route.
I think it should be done at component level. Otherwise it will be too hard to implement it.
Because there’s no Head component in Remix that will not be possible I imagine, if you want to preload the image you can add it to the LinksFunction of the route.
How does the
<Link prefetch="render" />
stuff work then? Couldn't we do something similar?
It renders the <link rel="prefetch" />
tags below the <a>
not in the <head>
but the difference is that you are prefetching data and assets for the next page, doing this:
<link rel="preload" as="image" href="/something.png" />
<img src="/something.png" />
Will not give you a lot of benefits because it's going to trigger the preload just when it's about to find the img tag so don't optimize anything. The point of preload would be to put it in the <head>
so before it starts to parse the body it can start loading the image immediately and when you do <Link prefetch="render" />
(or prefetch="intent"
) it will prefetch the image before the page navigate.
Will not give you a lot of benefits because it's going to trigger the preload just when it's about to find the img tag so don't optimize anything. The point of preload would be to put it in the
<head>
so before it starts to parse the body it can start loading the image immediately and when you do<Link prefetch="render" />
(orprefetch="intent"
) it will prefetch the image before the page navigate.
Makes sense. Surely we can figure out a way to allow components down tree to jam themselves up into <head>
right? Requiring someone to specify the images to preload in links()
seems kind of sad, especially since we want to put the generated imagesrcset
in there, not just the image path.
@ryanflorence any news on this?
Thanks for your great work!
Next has this actually useful Image component which wraps the
img
tag to optimize how you use it and enforce good practices like providing a width and height to avoid layout shifts.Remix used to have this
img:
prefix on the imports to get an object instead of a string and that object contained data like the width, height, srcset, placeholder, etc. based on query params. This was removed on the migration to esbuild.If it's hard to add that way to import adding the component could be a good fallback to have a way to optimize image load.
Component Interface
The component should be used as a drop-in replacement of the
img
tag, like Form withform
.And just with that it should be enough to start getting the benefit. Other options could be:
The loader should also be configured in the remix.config file so you don't need to add it individually. Remix could come with some built-in image loaders for services like Cloudinary, Cloudflare Image Resizing (when creating a new project with Cloudflare adapter this could come as the default) and maybe a custom one with some hidden endpoint created by Remix.
Special import
If adding back the support to
img:
imports, or usingassert { type: "image" }
in a future, is possible the object returned by the import could come with the same interface the Image component expects, that way you can use it like this:In this case the usage will be simplified a lot, you import and pass the imported to the component and it should magically work.