Optimizing Image Performance Using Next.js Image, plaiceholder, and Custom MDX Components - Modern Next.js Blog Series #22
- Published on
This article is also published at it 邦幫忙 2022 iThome Ironman Contest
Website performance bottlenecks are often due to slow image loading speeds. To enhance reader experience and SEO scores, this article will focus on optimizing in-text image performance!
The code changes for this article are as follows: https://github.com/Kamigami55/nextjs-tailwind-contentlayer-blog-starter/compare/day21-custom-link...day22-custom-image
Adding a Customized <CustomImage/>
to Optimize Image Loading Speed
Installing Related Packages
Copied!pnpm add image-size plaiceholder sharp unist-util-visit
Allowing Next.js Image to Use webp, avif Formats
Modify next.config.mjs
by adding an images section:
Copied!import { withContentlayer } from "next-contentlayer"; /** @type {import('next').NextConfig} */ const nextConfig = withContentlayer({ // ... // Add images section images: { // Enable modern image formats formats: ["image/avif", "image/webp"], }, }); export default nextConfig;
Using Custom imageMetadata rehype Plugin to Add Length and Width Attributes and LQIP to Images
Add src/plugins/imageMetadata.ts
:
Copied!// Custom rehype plugin to add width and height to local images // To make Next.js <Image/> work // Ref: https://kylepfromer.com/blog/nextjs-image-component-blog // Similar structure to: // https://github.com/JS-DevTools/rehype-inline-svg/blob/master/src/inline-svg.ts import imageSize from "image-size"; import path from "path"; import { getPlaiceholder } from "plaiceholder"; import { Node, visit } from "unist-util-visit"; import { promisify } from "util"; const sizeOf = promisify(imageSize); /** * An `<img>` HAST node */ interface ImageNode extends Node { type: "element"; tagName: "img"; properties: { src: string; height?: number; width?: number; base64?: string; }; } /** * Determines whether the given HAST node is an `<img>` element. */ function isImageNode(node: Node): node is ImageNode { const img = node as ImageNode; return ( img.type === "element" && img.tagName === "img" && img.properties && typeof img.properties.src === "string" ); } /** * Filters out non absolute paths from the public folder. */ function filterImageNode(node: ImageNode): boolean { return node.properties.src.startsWith("/"); } /** * Adds the image's `height` and `width` to its properties. */ async function addMetadata(node: ImageNode): Promise<void> { const res = await sizeOf( path.join(process.cwd(), "public", node.properties.src) ); if (!res) throw Error(`Invalid image with src "${node.properties.src}"`); const { base64 } = await getPlaiceholder(node.properties.src, { size: 10 }); // 10 is to increase detail (default is 4) node.properties.width = res.width; node.properties.height = res.height; node.properties.base64 = base64; } /** * This is a Rehype plugin that finds image `<img>` elements and adds the height and width to the properties. * Read more about Next.js image: https://nextjs.org/docs/api-reference/next/image#layout */ export default function imageMetadata() { return async function transformer(tree: Node): Promise<Node> { const imgNodes: ImageNode[] = []; visit(tree, "element", (node) => { if (isImageNode(node) && filterImageNode(node)) { imgNodes.push(node); } }); for (const node of imgNodes) { await addMetadata(node); } return tree; }; }
Modify contentlayer.config.ts
to apply the above-written imageMetadata rehype plugin:
Copied!import imageMetadata from "./src/plugins/imageMetadata"; // ... export default makeSource({ // ... mdx: { rehypePlugins: [ // ... imageMetadata, // For adding image metadata (width, height) ], }, });
Add src/components/CustomImage.tsx
:
Copied!import Image, { ImageProps } from "next/image"; type Props = ImageProps & { base64?: string }; export default function CustomImage({ src, height, width, base64, alt, ...otherProps }: Props) { if (!src) return null; if (typeof src === "string" && (!height || !width)) { return ( // eslint-disable-next-line @next/next/no-img-element <img src={src} height={height} width={width} alt={alt} {...otherProps} /> ); } return ( <Image layout="responsive" src={src} alt={alt} height={height} width={width} sizes="(min-width: 40em) 40em, 100vw" placeholder={base64 ? "blur" : "empty"} blurDataURL={base64} {...otherProps} /> ); }
Modify src/lib/mdxComponents.ts
so that all imgs in MDX use CustomImage for rendering:
Copied!import CustomImage from "@/components/CustomImage"; // ... // Custom components/renderers to pass to MDX. const mdxComponents = { // ... img: CustomImage, }; export default mdxComponents;
Results
Done! Use pnpm dev
, and enter any article with images to see the loading speed has improved!
A blurred version of the image will also be displayed while loading, informing readers that an image will appear there, also preventing layout shifts.
References
Next Article
Congratulations on customizing in-text images and speeding up loading!
The code changes for this article are as follows: https://github.com/Kamigami55/nextjs-tailwind-contentlayer-blog-starter/compare/day21-custom-link...day22-custom-image
In the next article, we will use another method to optimize the perceived page transition speed for readers, adding a page transition progress bar with nprogress!