Definition
If a tall screenshot in a blog post has ever appeared cropped top and bottom, or the text you were reading jumped down the moment an image loaded, this is the concept you need.
Intrinsic dimensions are the original pixel size the image file itself carries. <img> elements produced from markdown or CMS bodies usually have no width/height, so the browser cannot reserve space before the image loads. Opening the local image file at build time, reading its real dimensions, and injecting them as attributes solves space reservation and true-ratio rendering at once.
Why it matters
An image without dimensions has zero height before it loads. Arriving late, it pushes the content below it down — measured as Cumulative Layout Shift (CLS), which hurts both user experience and search ranking.
The usual response is aspect-ratio: 16 / 9 on the container. It stops the shift, but it has a price: portrait images, long screenshots, and square diagrams get cropped. That is the limit of guessing the ratio. The real fix is to stop guessing and read it from the source; for local assets you can read the file at build time, so the runtime cost and the network cost are both zero.
How it works
- Markdown is converted into an HTML tree (hast).
- A plugin walks the tree looking for
imgnodes. - If
srcis a local asset path (/images/...and the like), it resolves to the real file underpublic/. - Width and height are read from the image header (no full decode required).
- The
width/heightattributes are injected onto the node. - The browser derives
aspect-ratiofrom those two attributes and reserves exactly the right space before loading. - CSS gives
height: autoonly to images that have dimensions, so they render at their true ratio; images without them keep the existing fallback.
| State | Space reserved | Ratio | Result |
|---|---|---|---|
| No dimensions | No | Undetermined | Layout shift on load |
Fixed aspect-ratio fallback | Yes | Forced 16:9 | No shift, portrait images cropped |
| Intrinsic dimensions injected | Yes | Original | Neither shift nor cropping |
In practice
// rehype plugin (build time)
import { visit } from "unist-util-visit";
import imageSize from "image-size";
import { readFileSync } from "node:fs";
import path from "node:path";
export function rehypeImageSize({ publicDir }: { publicDir: string }) {
return (tree: Root) => {
visit(tree, "element", (node) => {
if (node.tagName !== "img") return;
const src = String(node.properties?.src ?? "");
if (!src.startsWith("/")) return; // leave external URLs alone
if (node.properties?.width) return; // respect an explicit value
try {
const file = readFileSync(path.join(publicDir, src));
const { width, height } = imageSize(file);
if (width && height) Object.assign(node.properties!, { width, height });
} catch {
// missing file → let the fallback handle it; never break the build
}
});
};
}
/* only dimensioned images render at true ratio; the rest keep the crop fallback */
.prose img[width][height] {
height: auto;
max-width: 100%;
}
.prose img:not([width]) {
aspect-ratio: 16 / 9;
object-fit: cover;
}
The unit test pins down "local images get dimensions, external URLs are untouched."
Trade-offs
- Build-time injection: zero runtime cost, zero burden on the user's device. In exchange, build time grows with the number of images, and it only applies to local assets.
- Runtime measurement (applying the ratio in
onLoad): works for external URLs, but it is already too late — the layout shift has happened by then. - Fixed-ratio fallback: the cheapest to implement, but it damages content. Keep it only as a safety net for images whose dimensions cannot be obtained.
- Image CDN parameters (
?w=1200&h=800): convenient because the dimensions are readable from the URL, but it ties you to a vendor.
When not to use it
- Bodies where every image is an external URL — the build cannot read the files, so you need another strategy (CDN metadata, recording dimensions at upload time).
- Images deliberately placed in a fixed-ratio thumbnail grid — there, cropping is the design, not a bug.
Common mistakes
- Deleting the fallback. There will always be images whose dimensions cannot be read. When the new path fails, it must fall back safely to the old behaviour.
- Mistaking
width/heightfor CSS pixels. These attributes are the source dimensions used for ratio calculation; the actual display size is decided by CSS. - Forgetting
height: auto. Setting onlywidth: 100%while leaving the height attribute in place squashes the image. - Trying to fetch external URLs at build time. The build becomes network-dependent, slower, and intermittently fails in CI.
- Putting
loading="lazy"on every image. Above-the-fold images end up slower — give only the herofetchpriority="high".
Related concepts
- cls-skeleton-layout-reservation — preventing layout shift by reserving space
- lqip-blur-placeholder-ssr — filling the reserved space with a low-resolution preview
- carousel-viewport-image-deferral — deferring loads for images outside the viewport
- server-image-proxy-transcoding-cache — server-side image transformation and caching