Βελτιστοποίηση CLS: Πλήρης Οδηγός για Cumulative Layout Shift το 2026
Πλήρης οδηγός για να μειώσετε το Cumulative Layout Shift κάτω από 0.1. Καλύπτει aspect-ratio, font fallbacks με size-adjust, bfcache, View Transitions και debugging με το νέο Layout Shift Culprits insight στο Chrome DevTools.
Το Cumulative Layout Shift (CLS) είναι ένα Core Web Vital που μετράει το άθροισμα των απρόσμενων μετατοπίσεων του layout κατά τη διάρκεια ζωής της σελίδας, και για να περάσετε το όριο της Google, πρέπει το CLS σας να είναι κάτω από 0.1 στο 75ο εκατοστημόριο των πραγματικών χρηστών. Στις 50+ σελίδες που έχω προφάιλει το τελευταίο εξάμηνο, το CLS διορθώνεται σχεδόν πάντα με τέσσερις παρεμβάσεις: σταθερές διαστάσεις εικόνας, σωστή φόρτωση γραμματοσειρών, αποφυγή injected DOM πάνω από το viewport, και χρήση του bfcache. Σε αυτόν τον οδηγό δείχνω το ακριβές trace κάθε περίπτωσης.
Το CLS μετριέται σε session windows 5 δευτερολέπτων με μέγιστο gap 1 δευτερολέπτου. Δεν είναι πια απλό άθροισμα, όπως πριν το 2021.
Ο στόχος είναι CLS < 0.1 στο p75 (καλό), 0.1 έως 0.25 χρειάζεται βελτίωση, > 0.25 κακό.
Οι 4 κύριες αιτίες: εικόνες/βίντεο χωρίς διαστάσεις, web fonts με FOUT/FOIT, δυναμικά injected elements (banners, ads, embeds), και late-loading CSS.
Το aspect-ratio CSS property και τα size-adjust font descriptors είναι οι δύο πιο γρήγορες νίκες το 2026.
Το bfcache εξαλείφει το CLS σε back/forward navigations. Βεβαιωθείτε ότι δεν το μπλοκάρετε με unload handlers ή Cache-Control: no-store.
Χρησιμοποιήστε το νέο Layout Shift Culprits insight στο Chrome DevTools Performance panel για να βρείτε ακριβώς ποιο element προκάλεσε κάθε shift.
Τι είναι το CLS και πώς υπολογίζεται
Το Cumulative Layout Shift καταγράφει κάθε unexpected shift ενός ορατού element και του αναθέτει ένα score = impact fraction × distance fraction. Το impact fraction είναι το ποσοστό του viewport που επηρεάστηκε, ενώ το distance fraction είναι η μεγαλύτερη μετατόπιση διαιρεμένη με τη μεγαλύτερη διάσταση του viewport. Δύο εικόνες ύψους 200px που πέφτουν 100px σε ένα viewport 1000px ψηλό δίνουν score περίπου 0.04, μέσα στο όριο, αλλά μόνο για ένα session window.
Από το 2021 και μετά, η μέτρηση γίνεται σε session windows: σύνολα shifts που συμβαίνουν με λιγότερο από 1 δευτερόλεπτο μεταξύ τους και διαρκούν το πολύ 5 δευτερόλεπτα. Το τελικό CLS είναι το μέγιστο από όλα τα session windows της σελίδας. Πρακτικά, αυτό σημαίνει ότι μια ξαφνική ριπή τριών shifts μέσα σε 4 δευτερόλεπτα μετράει σαν ένα μεγάλο score, ενώ τρεις μεμονωμένες ριπές σε διάστημα 30 δευτερολέπτων δίνουν τη χειρότερη ως τελική τιμή. Σύμφωνα με τα επίσημα docs του web.dev για το CLS, shifts που συμβαίνουν εντός 500ms από user input (click, keypress) εξαιρούνται. Γι' αυτό το άνοιγμα ενός accordion δεν μετράει.
Στο trace του DevTools, κάθε layout shift εμφανίζεται ως μωβ block στο Experience track. Από Chrome 122+, το block δείχνει και τα ακριβή elements που μετακινήθηκαν, οπότε δεν χρειάζεται πια να σκαλίζετε PerformanceObserver entries με το χέρι.
Ποιο είναι ένα καλό CLS score το 2026;
Ένα CLS κάτω από 0.1 θεωρείται «καλό» από την Google στο 75ο εκατοστημόριο των field data (CrUX). Από 0.1 έως 0.25 είναι «χρειάζεται βελτίωση» και πάνω από 0.25 είναι «κακό». Αυτά τα thresholds δεν έχουν αλλάξει από τότε που το CLS μπήκε στα Core Web Vitals, και η Google τα έχει ξανα-επικυρώσει στο roadmap του 2026.
Δύο πράγματα που μπερδεύουν συχνά τους developers:
Lab vs field: Το Lighthouse μετράει CLS μόνο για 6 δευτερόλεπτα μετά το load. Πραγματικοί χρήστες σκρολάρουν, ανοίγουν menus, περιμένουν ads. Αν το Lighthouse δείχνει 0.02 αλλά το CrUX 0.18, το πρόβλημα είναι σχεδόν πάντα κάτι που γίνεται μετά το load (third-party widget, lazy banner, ή font swap που καθυστερεί λόγω αργού δικτύου).
p75, όχι μέσος όρος: Η Google κοιτάει το score του χειρότερου 25% των επισκέψεων. Αυτό σημαίνει ότι μια σελίδα με μέσο CLS 0.05 μπορεί κάλλιστα να αποτύχει, αν το 30% των χρηστών έχει αργές συνδέσεις που εκθέτουν race conditions.
Στην πράξη, στοχεύω πάντα σε CLS < 0.05 στο lab. Έτσι έχω buffer για τους πραγματικούς χρήστες με αργά δίκτυα, παλιές συσκευές, και third-party scripts που δεν ελέγχω.
Διόρθωση CLS από εικόνες και βίντεο
Το #1 αίτιο CLS που βλέπω σε reviews είναι εικόνες χωρίς width και height attributes. Όταν το browser ξεκινάει το layout πριν φορτώσει η εικόνα, δεσμεύει 0 pixels. Όταν η εικόνα φτάνει, ολόκληρο το κάτω content πετάγεται κάτω. Η λύση είναι τετριμμένη αλλά κρίσιμη:
<!-- Λάθος: το browser δεν ξέρει το ύψος μέχρι να φορτώσει -->
<img src="/hero.webp" alt="Product hero">
<!-- Σωστό: width και height υπαγορεύουν aspect-ratio αυτόματα -->
<img src="/hero.webp" alt="Product hero" width="1200" height="630">
Από το 2020 και μετά, όλα τα modern browsers χρησιμοποιούν τα width/height attributes για να υπολογίσουν το aspect-ratio πριν φορτώσει το asset. Αυτό λειτουργεί ακόμα και αν το CSS κάνει την εικόνα width: 100%, αφού το ύψος προσαρμόζεται διατηρώντας τις αναλογίες.
Για responsive images με <picture> και διαφορετικά crops ανά breakpoint, χρησιμοποιήστε CSS aspect-ratio directly:
.hero-image {
width: 100%;
aspect-ratio: 16 / 9; /* Δεσμεύει χώρο πριν φορτώσει το asset */
background-color: #1a1a1a; /* Optional skeleton χρώμα */
object-fit: cover;
}
Για embedded videos (YouTube iframes, Vimeo, custom players), το aspect-ratio είναι πάλι ο σωστός τρόπος. Μην βασίζεστε στο παλιό padding-bottom hack, γιατί έχει edge cases με flexbox children. Για κάθε image-heavy σελίδα, δείτε επίσης τον οδηγό μας για βελτιστοποίηση LCP, μιας και συχνά οι ίδιες παρεμβάσεις (preload, fetchpriority, AVIF) επηρεάζουν και τις δύο μετρικές.
Πώς να αποφύγετε CLS από web fonts
Τα web fonts προκαλούν CLS με δύο μηχανισμούς: FOUT (Flash of Unstyled Text), όταν το fallback font είναι ορατό μέχρι να φορτώσει το custom font και η εναλλαγή αλλάζει το layout, και FOIT (Flash of Invisible Text), όταν το browser κρύβει το κείμενο μέχρι 3 δευτερόλεπτα και μετά κάνει swap. Και τα δύο μπορούν να επιφέρουν shift score 0.05 έως 0.2 σε μια text-heavy σελίδα.
Η λύση το 2026 είναι ο συνδυασμός τριών τεχνικών:
font-display: optional ή swap ανάλογα με το brand requirement
preload των κρίσιμων font files
size-adjust και τα νέα font metric overrides για perfect fallback matching
/* Fallback font με matched metrics, μηδενικό CLS κατά το swap */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107.4%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
font-weight: 100 900;
}
body {
font-family: 'Inter', 'Inter Fallback', system-ui, sans-serif;
}
Τα size-adjust, ascent-override, descent-override και line-gap-override descriptors υποστηρίζονται σε όλους τους major browsers από το 2022. Με σωστές τιμές, ο fallback καταλαμβάνει ακριβώς τον ίδιο χώρο με το πραγματικό font, οπότε το swap γίνεται αόρατα. Για να βρείτε τις σωστές τιμές, χρησιμοποιήστε το εργαλείο Fallback Font Generator ή τη βιβλιοθήκη next/font που τα υπολογίζει αυτόματα.
Banners, ads και δυναμικό περιεχόμενο
Cookie banners, newsletter modals, και ad slots που εισάγονται μετά το load είναι η νούμερο 2 αιτία CLS σε production. Έχω δει e-commerce homepages με LCP 1.2s και CLS 0.45. Όλο το shift, από έναν consent banner που εμφανιζόταν 800ms αργότερα και έσπρωχνε όλο το main content κάτω. Honestly, αυτό το pattern το έχω συναντήσει τόσες φορές που πρώτο πράγμα κοιτάω σε κάθε audit.
Τρεις αρχές που εφαρμόζω πάντα:
Δεσμεύστε χώρο εκ των προτέρων. Αν ένα ad slot είναι πιθανό να φορτώσει 300×250, ορίστε min-height: 250px στο container από την αρχή. Καλύτερα να δείτε ένα κενό grey box για 200ms παρά shift 0.2.
Τοποθετήστε non-critical UI κάτω από το fold ή με fixed positioning. Cookie banners που εμφανίζονται σαν position: fixed overlay στο bottom δεν προκαλούν shift, γιατί δεν επηρεάζουν το document flow.
Προτιμήστε CSS transforms για animations. Το transform: translateY() και το opacity δεν επηρεάζουν το layout και δεν μετράνε ως shifts. Animations με top, margin, ή height το κάνουν.
Για banners που πρέπει να εμφανιστούν inline (π.χ. promo bar πάνω από το header), προφορτώστε τη μέγιστη πιθανή διάσταση:
.promo-bar {
min-height: 48px; /* Κρατάει χώρο πριν φορτώσει το JS */
contain: layout style; /* Απομονώνει το layout από το υπόλοιπο document */
}
.promo-bar:empty {
display: none; /* Δεν δείχνει empty bar αν δεν υπάρχει promo */
}
Το contain: layout είναι ένα από τα πιο underused properties το 2026. Λέει στο browser ότι τα παιδιά αυτού του element δεν επηρεάζουν το layout οποιουδήποτε άλλου element έξω από αυτό. Όταν αλλάζει το ύψος ενός contained element, μόνο το ίδιο και τα descendants του ανυπολογίζονται.
Bfcache, View Transitions και content-visibility
Το back/forward cache (bfcache) είναι η πιο underrated CLS optimization. Όταν ο χρήστης πατάει back, αν η σελίδα είναι στο bfcache, ο browser την επαναφέρει instant: μηδέν layout, μηδέν shifts, μηδέν CLS contribution. Το Chrome δίνει μάλιστα ξεχωριστό CLS score για bfcache restorations, σχεδόν πάντα 0.
Πράγματα που μπλοκάρουν το bfcache:
unload ή beforeunload event listeners (χρησιμοποιήστε pagehide και visibilitychange αντί αυτών)
Cache-Control: no-store response header
Ανοιχτές IndexedDB transactions ή WebSocket connections (μπορεί)
HTTP authentication headers σε ορισμένα browsers
Δείτε το eligibility status στο DevTools Application panel → Back/forward cache. Το Chrome δίνει συγκεκριμένο λόγο γιατί αποτυγχάνει και πώς να το διορθώσετε.
Το View Transitions API (Same-document και Cross-document από Chrome 126) είναι ο νέος τρόπος για smooth navigation animations χωρίς CLS. Αντί να αφήσετε το browser να κάνει abrupt re-layout, ορίζετε explicit transitions:
// Same-document transition με zero CLS
if (document.startViewTransition) {
document.startViewTransition(() => {
updateDOM(); // Η αλλαγή γίνεται μέσα σε snapshot, χωρίς να μετράει για CLS
});
}
Τέλος, το content-visibility: auto λέει στο browser να skip το rendering για off-screen sections (μειώνει layout cost) και, σε συνδυασμό με contain-intrinsic-size, δεσμεύει σταθερό χώρο για το placeholder:
.below-fold-section {
content-visibility: auto;
contain-intrinsic-size: 0 800px; /* Ψεύτικο ύψος μέχρι να γίνει render */
}
Χωρίς το contain-intrinsic-size, το content-visibility μπορεί να προκαλέσει shift όταν ο χρήστης σκρολάρει. Το element έχει 0 ύψος όταν είναι off-screen και αναπτύσσεται όταν μπαίνει στο viewport.
Πώς να βρείτε τι προκαλεί CLS με DevTools
Ο πιο γρήγορος δρόμος για να εντοπίσετε τον CLS culprit είναι το Performance panel του Chrome DevTools:
Throttle network σε Slow 4G και CPU σε 4× slowdown, ώστε να αναπαράγετε τις συνθήκες των πραγματικών χρηστών
Πατήστε Record, κάντε reload, περιμένετε 5 δευτερόλεπτα μετά το load
Στο Insights panel (κάτω δεξιά), επιλέξτε «Layout shift culprits»
Από Chrome 122 και μετά, το insight δείχνει για κάθε shift: το ακριβές DOM node, το unsized media που το προκάλεσε, τυχόν injected stylesheets, και τα font swap events. Δεν χρειάζεται πια να γράφετε custom PerformanceObserver για βασικό debugging.
Αν χρειάζεστε continuous monitoring στο production, χρησιμοποιήστε τη βιβλιοθήκη web-vitals της Google με την attribution build για να στείλετε στο analytics όχι μόνο το score αλλά και το largest shift target:
Με αυτό το payload, μπορείτε να φτιάξετε ένα dashboard που λέει «το 40% του p75 CLS μας έρχεται από .cookie-banner», ώστε να επιτεθείτε στο πρόβλημα στοχευμένα, όχι σε όλη τη σελίδα. Παρόμοια προσέγγιση δουλεύει και για άλλα Core Web Vitals: δείτε τον οδηγό μας για βελτιστοποίηση INP που βασίζεται στο ίδιο pattern με attribution.
Διόρθωση CLS σε mobile
Το mobile CLS είναι σχεδόν πάντα χειρότερο από desktop για τρεις λόγους: τα viewports είναι στενότερα οπότε το distance fraction μεγαλώνει, τα δίκτυα είναι αργότερα οπότε τα assets φτάνουν αργά, και τα system fonts διαφέρουν τόσο πολύ μεταξύ Android και iOS που τα custom fonts προκαλούν μεγαλύτερα swaps.
Συγκεκριμένες παρεμβάσεις που δουλεύουν καλύτερα σε mobile:
Σταθερή viewport height με dvh. Στο iOS Safari, η μπάρα URL μεγαλώνει/μικραίνει και αλλάζει το 100vh. Χρησιμοποιήστε 100dvh (dynamic viewport) για hero sections, αφού το browser υπολογίζει σωστά το ύψος και δεν προκαλεί shift όταν αλλάζει η toolbar.
Pre-define heights για collapsible menus. Mobile menus που εμφανίζονται με display: none → block προκαλούν τεράστιο shift. Χρησιμοποιήστε visibility + transform ή τα νέα interpolate-size: allow-keywords animations για smooth height transitions χωρίς CLS impact.
Test με slow 3G throttle. Πολλά shifts εμφανίζονται μόνο όταν τα assets αργούν αρκετά ώστε να γίνει visible η non-styled state. Στο fast Wi-Fi δεν θα τα δείτε ποτέ.
Συχνές Ερωτήσεις
Ποιο είναι ένα καλό CLS score;
Ένα CLS κάτω από 0.1 θεωρείται καλό στο 75ο εκατοστημόριο των πραγματικών χρηστών (CrUX data). Η ζώνη 0.1 έως 0.25 χρειάζεται βελτίωση και πάνω από 0.25 είναι κακό. Στο lab, στοχεύω σε CLS < 0.05 για να έχω buffer.
Γιατί έχω καλό CLS στο Lighthouse αλλά κακό στο CrUX;
Το Lighthouse μετράει μόνο τα πρώτα 6 δευτερόλεπτα σε ιδανικές συνθήκες. Πραγματικοί χρήστες σκρολάρουν, ανοίγουν menus και βιώνουν third-party scripts να φορτώνουν αργά. Συνήθεις αιτίες απόκλισης: cookie banners που εμφανίζονται μετά το load, lazy-loaded ads, και font swaps σε αργά δίκτυα.
Πώς αποτρέπω layout shift από web fonts;
Χρησιμοποιήστε font-display: swap ή optional, preload τα κρίσιμα font files, και ορίστε size-adjust με ascent-override στο fallback @font-face ώστε να ταιριάζει ακριβώς με το πραγματικό font. Έτσι το swap γίνεται χωρίς αλλαγή στις διαστάσεις του κειμένου.
Επηρεάζει το lazy loading το CLS;
Όχι από μόνο του, αρκεί οι lazy-loaded εικόνες να έχουν explicit width και height attributes ή CSS aspect-ratio. Χωρίς διαστάσεις, μια lazy image που μπαίνει στο viewport προκαλεί ένα shift score = ύψος εικόνας / ύψος viewport, εύκολα 0.2+ σε mobile.
Μετράει το CLS shifts που γίνονται μετά από user interaction;
Όχι. Shifts που συμβαίνουν εντός 500ms από user input (click, tap, keypress) εξαιρούνται από τη μέτρηση. Γι' αυτό το άνοιγμα ενός accordion ή dropdown δεν αυξάνει το CLS. Αλλά shifts από scroll μετράνε, γιατί το scroll δεν θεωρείται «user input» σε αυτό το context.