A logo rendered at 15 pixels looked slightly thicker in Chrome than it did in Firefox.

Guillaume Técher first did the sensible thing: he replaced the JPEG with an SVG. The visual problem disappeared.1

Then he did the less sensible, more interesting thing and investigated why the same tiny JPEG could look different across browsers.

The trail goes down into image decoding. When Chrome displays a JPEG far below its original dimensions, Skia can ask libjpeg-turbo to decode a reduced version directly, rather than expanding every source pixel and immediately shrinking the bitmap afterward.12

At very small sizes, that optimization can become visible.

JPEG already organizes information by frequency

JPEG represents image blocks using a discrete cosine transform, or DCT. The resulting coefficients roughly describe slower and faster variations inside each block.

Broad, smooth areas live mostly in lower-frequency information. Fine textures and sharp edges need more of the higher-frequency components.1

When a large image is displayed at a few dozen pixels, much of that fine detail is going to disappear anyway.

So there is an elegant shortcut: do not fully reconstruct information that the next scaling operation will throw away.

libjpeg-turbo explicitly supports IDCT scaling during decompression. Its documentation lists factors including 1/8, 1/4, 3/8, 1/2, 5/8, 3/4, and 7/8.3

Skia looks for a size the decoder can produce

SkJpegCodec shows the mechanism directly. When checking whether a requested size is natively supported, Skia tries fractions from 8/8 down toward 1/8, then sets libjpeg’s scale_num and scale_denom values when it finds a match.2

So the pipeline is not always:

  1. decode the entire JPEG;
  2. allocate a full-size bitmap;
  3. shrink that bitmap to 15 pixels.

It can begin with a JPEG already decoded at a fraction of its original size, then use the graphics engine’s scaling step to reach the exact requested dimensions.12

For photographs, that is usually a good trade: less memory and less work for detail that would not survive the final display size.

For a tiny icon with precise edges, the compromise is easier to see.

At one-eighth scale, some detail never makes the trip

In Técher’s example, the JPEG was reduced enough for the Chrome result to look visibly heavier than the Firefox version.1

His original explanation focused mainly on partial IDCT decoding. After discussion on Hacker News, he added a correction: the scaling algorithm used after decoding also plays a significant role.1

That distinction matters. This is not a simple rule where “Chrome always drops high frequencies and Firefox does not.” The final appearance depends on multiple stages: which native JPEG scale is decoded, then how that intermediate image is resampled to the exact CSS size.

The safer conclusion is narrower: two valid rendering pipelines can make different compromises, and those compromises become visible when the source image was already a poor fit for the job.

A JPEG logo is a tiny piece of technical debt

JPEG was built for photographic imagery. It deliberately discards information where that loss is usually hard to notice.

Icons, logos, and thin line art have the opposite requirement. Every edge matters. A one-pixel change can alter the apparent weight of lettering or close a small gap.

SVG avoids much of that ambiguity for vector shapes. PNG can also be more predictable when the source genuinely needs to remain a small raster image with sharp edges.

Técher had therefore fixed the practical problem before understanding the apparent rendering bug: replacing the JPEG logo with SVG was the right move.1

Optimizations are not invisible, only well hidden

The more general lesson is about rendering work we normally never see.

We often talk about optimization as if it means doing exactly the same computation faster. A renderer can instead save work by not calculating information it expects the final result to discard.

Most of the time that decision is imperceptible. That is what makes it a useful optimization.

Then someone displays the wrong JPEG at 15 pixels and the compromise leaks through.

One small stroke gets heavier, and behind it you suddenly see DCT blocks, Skia, libjpeg-turbo, and an entire rendering pipeline that a plain <img> had politely hidden from you.