Skip to content

Store image aspect ratio on puzzles for CLS optimization #84

@JanMikes

Description

@JanMikes

Problem

Currently, puzzle thumbnail images use a square aspect ratio assumption (width=80 height=80) for the HTML width/height attributes. These attributes tell the browser how much space to reserve before the image loads, preventing Cumulative Layout Shift (CLS).

Since most puzzle images are not square (e.g. 135×200 portrait), the assumed dimensions don't match reality. This means either:

  • The browser reserves wrong space and layout shifts when the image loads
  • We rely on inline max-height CSS to constrain overflow, which works visually but doesn't give the browser the correct aspect ratio hint

Solution

Store the image aspect ratio (width / height) as a single float column on the puzzle entity (e.g. imageRatio).

Why ratio instead of width+height

A single float is sufficient. Given the ratio and a target max display size, we can calculate exact width and height at render time:

if ratio > 1 (landscape):  width = size, height = size / ratio
if ratio < 1 (portrait):   height = maxHeight, width = maxHeight * ratio
if ratio = 1 (square):     width = height = size

Example: Dino Frozen image (135×200, ratio=0.675) at size=80 → width="54" height="80"

Implementation steps

  1. Add imageRatio (nullable float) column to the Puzzle entity
  2. On image upload, calculate and store width / height from the uploaded image dimensions
  3. Write a migration to backfill existing puzzles (read dimensions from stored S3 images or thumbnails)
  4. Update LazyImageTwigExtension::lazyPuzzleImage() to accept the ratio and calculate accurate width/height attributes
  5. Pass the ratio from templates/components that render puzzle images

Impact

  • Eliminates CLS for puzzle image loading across all pages
  • Removes the need for the inline style="max-height:Xpx" workaround
  • Better Lighthouse/PageSpeed scores

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions