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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions