L Art du Pré-chargement : Éliminer le "Flash" de Thème avec JavaScript et CSS
Δημοσιεύτηκε 15 November 2025
Περίληψη
Σε αυτό το τεχνικό άρθρο βάθους, εξερευνούμε πώς να υλοποιήσουμε ένα επιλογέα θέματος (light/dark) χωρίς τον ενοχλητικό εφέ απόκλισης που εμφανίζεται κατά τη φόρτωση της σελίδας. Αναλύουμε αναλυτικά τον κύκλο απόδοσης του φυλλομετρητή, τις βαθείες αιτίες του FOUC (Flash of Unstyled Content), και προτείνουμε μια αξιόπιστη λύση βασισμένη στην συγχρονή προ-φόρτωση στο`<head>`. Αυτή η τεχνική εξασφαλίζει μια ομαλή και επαγγελματική εμπειρία χρήστη.
Το πρόβλημα: Αυτή η καταραμένη θεματική αναλάμψη
Ένα εφιάλτης για την εμπειρία χρήστη
Φανταστείτε τη σκηνή: Έχετε δώσει ώρες για τη σχεδίαση ενός υπέροχου σκούρο θέματος για την εφαρμογή σας στο διαδίκτυο. Τα χρώματα είναι απόλυτα ισορροπημένα, η αντίθεση είναι βέλτιστη, και οι χρήστες σας αγαπούν αυτή τη δυνατότητα. Ένα αισχυντικό πρόβλημα: κάθε φορά που φορτώνεται ξανά η σελίδα, για μια μικρή στιγμή, το προεπιλεγμένο φωτεινό θέμα εμφανίζεται πριν το σκοτεινό θέμα παίρνει τη θέση του.
Αυτό το 'flash' οπτικό, παρόλο που είναι σύντομο (συχνά λιγότερο από 100ms), είναι άμεσα αντιληπτό από το ανθρώπινο μάτι και δημιουργεί μια άνεστη εμπειρία. Για τους χρήστες που επέλεξαν το σκούρο θέμα για λόγους οπτικής ευκολίας ή προσβασιμότητας, αυτό το lampeυμα μπορεί να είναι βολικό, ειδικά σε ένα χαμηλά φωτισμένο περιβάλλον.
Το FOUC : Ένα Παλαιό Πρόβλημα Ιστού
Αυτό το φαινόμενο είναι μια παραλλαγή του λεγόμενου "Flash of Unstyled Content" (FOUC), ένα κλασικό πρόβλημα της ανάπτυξης ιστού που mająι τις ρίζες τους στα πρώτα χρόνια του CSS. Το FOUC εμφανίζεται όταν ο φυλλοτρέπτης εμφανίζει προσωρινά περιεχόμενο HTML χωρίς να έχουν εφαρμογεί τα CSS του, δημιουργώντας μια σάλη του μη στυλισμένου περιεχομένου.
Στο συγκεκριμένο μας σενάριο, δεν μιλάμε για ένα πλήρως μη στυλοποιημένο περιεχόμενο, αλλά για έναΦλας του λάθους θέματος(FOWT) - το περιεχόμενο είναι στυλό, αλλά με το λάθος θέμα. Είναι ιδιαίτερα ενοχλητικό επειδή δείχνει ότι η εφαρμογή μας "ξεχάσει" την προτίμηση του χρήστη σε κάθε φόρτωση σελίδας.
Επίπτωση στην Αντίληψη της Ποιότητας
Αυτό το πρόβλημα, αν και τεχνικό, έχει σημαντικές επιπτώσεις στην αντίληψη της ποιότητας της εφαρμογής σας:
Έλλειψη λάμψης: Το ανάμψι δίνει την εντύπωση μιας μη ολοκληρωμένης ή κακώς βελτιστοποιημένης εφαρμογής. Οι χρήστες συνήθως συνδέουν αυτές τις μικρές οπτικές αδυναμίες με ένα γενικό έλλειμα επαγγελματισμού.
διακοπή συνέπειαςΗ εφαρμογή φαίνεται να "ξεχνά" τις προτιμήσεις του χρήστη, δημιουργώντας αίσθηση ανασυγκρότησης μεταξύ του περιβάλλοντος και των προσδοκιών.
Οπτική κόπωσηΓια τους χρήστες που είναι ευαίσθητοι στο φως ή παiscono από μιγρενιές, αυτή η αστραπιά φωτός μπορεί να είναι περισσότερο απλά ένα αισθητικό ανόνημα.
απόδοση που αντιλαμβάνεταιΙροντικά, ακόμα κι αν το site σας φορτώνεται γρήγορα, το ανάβασμα μπορεί να δημιουργήσει την εντύπωση μιας αργής ή ανταποκρινόμενης εφαρμογής.
Τεχνική ανάλυση του προβλήματος
Για να καταλάβεις πώς να λύσεις αυτό το πρόβλημα, πρέπει πρώτα να καταλάβεις γιατί προκύπτει. Το να αναπνέω εμφανίζεται λόγω μιας χρονικής διαφοράς μεταξύ τριών κρίσιμων γεγονότων στον κύκλο ζωής μιας ιστοσελίδας :
-
Το αρχικό parsing του HTMLΟ περιηγητής διαβάζει και αναλύει τη δομή της σελίδας σας
-
Η εφαρμογή των στυλ CSS: Ο περιηγητής εφαρμόζει τους κανόνες CSS και υπολογίζει την οπτική apeικόνιση
-
Η εκτέλεση του JavaScript: Ο κώδικας σας που αλλάζει το θέμα εκτελείται
Το πρόβλημα εμφανίζεται όταν το γεγονός 3 (εκτέλεση JavaScript) εμφανίζεται μετά από το πρόγραμμα περιήγησης που έχει ήδη ξεκινήσει ή τελειώσει το γεγονός 2 (εφαρμογή στυλ). Στον τότε χρόνο, το πρόγραμμα περιήγησης έχει πάρει μια απόφαση για ποιο θέμα θα εμφανιστεί, και ο κώδικάς σας έρχεται αργά ώστε να δεν μπορεί να τον επηρεάσει πριν από την πρώτη απόδοση.
Γιατί ένα κλασικό script δεν αρκεί;
Η αισθητική προσεγγίسة αλλά αναποδοτική
Η πιο φυσική προσέγγιση για έναν προγραμματιστή θα ήταν να τοποθετήσει ένα script στο τέλος του`<body>`ελέγχει το προτιμάμενο θέμα του χρήστη και το εφαρμόζει. Αυτή η προσέγγιση ακολουθεί τις παραδοσιακές καλύτερες πρακτικές του ιστού που συνιστάνουν τη φόρτωση των σεναρίων στο τέλος της σελίδας ώστε να μην εμποδίζει την εμφάνιση.
// À la fin de <body> - L'APPROCHE INSUFFISANTE
document.addEventListener('DOMContentLoaded', () => {
const theme = localStorage.getItem('preferred-theme');
if (theme === 'dark') {
document.documentElement.setAttribute('data-bs-theme', 'dark');
}
});
Αυτή η προσέγγιση φαίνεται λογική στην πρώτη ματιά. Περιμένουμε το DOM να είναι έτοιμο, μετά εφαρμόζουμε το θέμα. Απλό, ναι; Δυστυχώς, αυτή η απλότητα κρύβει ένα θεμελιώδες λάθος που σχετίζεται με το χρόνο του κύκλου απόδοσης του περιηγητή.
Κατανόηση του συμβάντος DOMContentLoaded
Το γεγονός`DOMContentLoaded`ενεργοποιείται όταν το αρχικό εγγράφο HTML έχει φορτωθεί πλήρως και αναλυθεί από τον περιηγητή,χωρίς αναμονήτο terminus της φόρτωσης των φύλλων στυλ, των εικόνων και των υποπλαίσίων. Είναι ένα σημαντικό σημείο για να κατανοήσεις.
Αυτό είναι η τυπική ακολουθία γεγονότων :
-
Ο περιηγητής αρχίζει να κατεβάζει το HTML
-
Αναλύει το HTML καθώς το λαμβάνει
-
Ανακαλύπτει τις ετικέτες`<link>`για τα CSS και αρχίζει να τα κατεβάζει
-
Ανακαλύπτει τις ετικέτες`<script>`και τα εκτελεί (ανάλογα με τον τύπο και τα χαρακτηριστικά τους)
-
Κατασκευάζει το DOM (Document Object Model)
-
Το γεγονός
DOMContentLoadedεκτιέθεται -
Συνεχίζει να εφαρμόζει τα στυλ και να κάνει το layout
-
Το πρώτο paint (εμφάνιση) προκύπτει
-
Το γεγονός`load`ενεργοποιείται όταν όλες οι πόροι φορτώνονται
Το πρόβλημα; Ανάμεσα στο βήμα 6 (DOMContentLoaded) και το βήμα 8 (πρώτο paint), ο φυλλομετρητής έχει ήδη πάρει αποφάσεις για πώς να εμφανίσει τη σελίδα. Αν το σενάριο αλλαγής θέματος σας εκτελείται στο βήμα 6, είναι ήδη πολύ αργά για να αποφύγετε μια αρχική εμφάνιση με τα προεπιλεγμένα στυλ.
Το πρόβλημα του Render Blocking
Στη πραγματικότητα, ο χρόνος είναι ακόμη πιο περίπλοκος. Τα σύγχρονα προγράμματα περιήγησης χρησιμοποιούν προηγμένες τεχνικές βελτιστοποίησης για να βελτιώσουν την αντιληπτή απόδοση. Προσπαθούν να κάνουν το πρώτο paint (First Contentful Paint) όσο πιο γρήγορα είναι δυνατόν ώστε ο χρήστης να δει κάτι στην οθόνη.
Τα CSS είναι "render-blocking" από προεπιλογή, το οποίο σημαίνει ότι ο περιηγητής περιμένει να κατεβάσει και να αναλύσει τα φύλλα στυλ πριν κάνει το πρώτο paint. Είναι λογικό: δεν θέλουμε να εμφανίζουμε μη στυλισμένο περιεχόμενο.
Αλλά εδώ είναι η παγίδα: όταν ο φυλλομετρητής εφαρμόζει αυτά τα CSS στυλ για πρώτη φορά, το κάνει βασισμένος στην τρέχουσα κατάσταση του DOM. Αν το χαρακτηριστικό`data-bs-theme`Δεν είναι ακόμα ορισμένο στην ετικέτα`<html>`, ο περιηγητής θα εφαρμόσει τα προεπιλεγμένα στijl (συνήθως το φωτεινό θέμα).
Επειδή, όταν το script σας εκτελείται και αλλάζει αυτό το attribute, ο φυλλομετρητής πρέπει :
-
Υπολογίστε ξανά όλα τα στυλ που επηρεάστηκαν από αυτήν την αλλαγή
-
Επανακατασκευάστε το layout εάν είναι απαραίτητο
-
Επαναβογιάστε τα επηρεασμένα στοιχεία
Αυτή η διαδικασία επανυπολογισμού και επαναρυμάνισης είναι αυτή που προκαλεί το ορατό διασπορά.
Οπτικοποίηση του προβλήματος
Για να κατανοήσουμε καλύτερα αυτή τη προβληματική ακολουθία, ας εξετάσουμε ένα λεπτομερές διάγραμμα ακολουθίας:
@startuml
actor Utilisateur
participant Navigateur
participant "Parser HTML" as Parser
participant "CSSOM" as CSS
participant "Script.js (fin de body)" as Script
participant "Render Engine" as Render
Utilisateur -> Navigateur: Demande la page
activate Navigateur
Navigateur -> Parser: Parse le HTML
activate Parser
Parser -> CSS: Découvre et charge les CSS
activate CSS
Parser -> Parser: Construit le DOM
Parser -> Navigateur: DOM Ready (DOMContentLoaded)
deactivate Parser
CSS -> CSS: Parse les feuilles de style
CSS -> Render: CSSOM Ready
deactivate CSS
activate Render
Render -> Render: Calcule les styles avec data-bs-theme=""
note right: Thème par défaut (light)
Render -> Navigateur: Premier Paint (Thème Clair)
note right: L'utilisateur voit le thème clair
deactivate Render
Navigateur -> Script: Exécute le script
activate Script
Script -> Script: Lit localStorage
Script -> Navigateur: setAttribute('data-bs-theme', 'dark')
deactivate Script
activate Render
Render -> Render: Recalcule les styles avec data-bs-theme="dark"
note right: Thème sombre
Render -> Navigateur: Repaint (Thème Sombre)
note right: Clignotement visible
deactivate Render
deactivate Navigateur
@enduml
Αυτό το διάγραμμα δείχνει σαφώς το πρόβλημα: το πρώτο paint εμφανίζεται πριν το script μας να έχει την ευκαιρία να ορίσει το σωστό θέμα. Ο επόμενος repaint δημιουργεί το ορατό αναλουλό.
Οι αναποτελεσματικές προσπάθειες λύσης
Πολλές προσεγγίσεις έχουν δοκιμαστεί για να λύσουν αυτό το πρόβλημα, αλλά οι περισσότερες έχουν τα δικά τους μειονεκτήματα:
Προσέγγιση 1: Αποκρύβει το περιεχόμενο μέχρι την φόρτωση
body {
opacity: 0;
transition: opacity 0.3s;
}
body.loaded {
opacity: 1;
}
Αυτή η προσέγγιση κρύβει όλο το περιεχόμενο μέχρι να ορίσει το JavaScript το σωστό θέμα. Το πρόβλημα; Αυτό καθυστερεί τεχνητά την εμφάνιση του περιεχομένου, δημιουργώντας την εντύπωση ενός πιο αργού site. Επιπλέον, αν το JavaScript είναι απενεργοποιημένο, ο χρήστης δεν βλέπτει καθόλου τίποτα!
Μέθοδος 2: Χρησιμοποιήστε ένα loader/spinner
Παρόμοια με την προσέγγιση 1, αλλά με ένα spinner φόρτωσης. Αυτό κρύβει το πρόβλημα, αλλά δεν βελτιώνει την πραγματική απόδοση και προσθέτει άχρηστο αντιληπτό καθυστέρηση.
Προσέγγιση 3: Προεπιλογή σε σκούρο θέμα
Ορισμένοι προγραμματιστές ορίζουν το σκοτεινό θέμα ως προεπιλογή στο CSS. Αυτό αποφεύγει το αναμπτύξιμο για τους χρήστες του σκοτεινού θέματος, αλλά δημιουργεί το αντίθετο πρόβλημα για τους χρήστες του φωτεινού θέματος !
Κανένας από αυτές τις προσεγγίσεις δεν είναι ικανοποιητικός, επειδή αντιμετωπίζουν το συμπτώμα αντί της ρίζας του προβλήματος.
Η αληθινή λύση : Να ενεργείτε πιο νωρίς
Η κλειδί για την επίλυση αυτού του προβλήματος είναι να καταλάβουμε ότι πρέπει να ορίσουμε το χαρακτηριστικό`data-bs-theme` πρινότι ο φυλλομετρητής δεν αρχίζει να εφαρμόζει τα CSS στυλ. Αυτό σημαίνει ότι το σενάριο μας πρέπει να εκτελεστεί νωρίτερα στον κύκλο ζωής της σελίδας, και αυτό είναι ακριβώς το οποίο θα εξερευνήσουμε στην επόμενη ενότητα.
Η λύση : Προωρό φόρτωση (Early Loading)
η θεμελιώδης αρχή
Η ελκυστική λύση στο πρόβλημα μας του αναβοσβενούς φαινομένου βασίζεται σε ένα απλό αλλά ισχυρό αρχή:Συγχρονισμός της κατάστασης της εφαρμογής με τη διαδικασία απόδοσης του περιηγητή. Αντί να περιμένουμε τη φόρτωση της σελίδας για να ορίσουμε το θέμα, πρέπει να το ορίσουμε.ενώτο φόρτωση, ακόμα και πριν εφαρμοσθούν τα στυλ CSS.
Αυτή η προσέγγιση ονομάζεται "Early Loading" ή "Synchronous Preloading" στον όρο της ανάπτυξης ιστού. Η ιδέα είναι να εκτελέσουμε τη λογική ανίχνευσης θέματος όσο το δυνατόν πιο νωρίς στον κύκλο ζωής της σελίδας, ιδανικά μέσα στην ετικέτα`<head>`, ακόμη και πριν το περιηγητή αρχίσει να κατεβάζει τα αρχεία CSS.
Γιατί το <head> είναι το Ιδεαλικό χώρο
Le `<head>`Ένα έγγραφο HTML επεξεργάζεται ακολουθιακά από τον περιηγέα, από πάνω προς τα κάτω. Κάθε στοιχείο επεξεργάζεται στη σειρά που εμφανίζεται. Αυτή η ιδιότητα είναι κρίσιμη για τη λύση μας.
Όταν ο περιηγητής συναντά μια ετικέτα`<script>`στόν`<head>`χωρίς τα χαρακτηριστικά`async` ou defer, il :
-
διακόπτει την ανάλυση του HTML
-
Κατεβάζε το script(εάν είναι εξωτερικό) ή το κρεβάτι (εάν είναι ενσωματωμένο)
-
Εκτέλεσε αμέσως το σενάριο
-
Επαναλήβει την ανάλυση του HTML
Αυτό το συμπεριφορικό, συχνά θεωρείται ως πρόβλημα απόδοσης (όθεν η συνήθης σύσταση να τοποθετούμε τα scripts στο τέλος της σελίδας), γίνεται ο σύμμαχος μας σε αυτή τη συγκεκριμένη περίπτωση. Τοποθετώντας το script μας για ανίχνευση θέματος στην αρχή του`<head>`, εγγυόμαστε ότι εκτελείται πριν ο φυλλομετρητής συναντήσει τις ετικέτες`<link>`των φύλλων στυλ μας.
Αρχιτεκτονική της Λύσης σε Τρεις Στρώσεις
Η πλήρης λύση μας αποτελείται από τρεις εξαρτημένες στρώσεις, κάθε μία παίζοντας έναν συγκεκριμένο ρόλο :
Επίπεδο 1: Διατήρηση (localStorage) Αυτό το στρώμα είναι υπεύθυνο για την αποθήκευση και την ανάκτηση της επιλογής του χρήστη μεταξύ των συνεδριών.
Επίπεδο 2 : Προωρή σύγχρονιση (ενσωματωμένο σενάριο στο <head>) Αυτό το επίπεδο συγχρονίζει την κατάσταση της εφαρμογής με το DOM πριν από την αρχική απόδοση.
Επιπέδος 3 : Ανταποκρινόμενοι Στυλ (CSS με επιλογείς ιδιοτήτων) Αυτό το επίπεδο καθορίζει τα οπτικά στυλ βασισμένα στην κατάσταση που ορίζεται από το επίπεδο 2.
Ας εξερευνήσουμε τώρα κάθε στρώση λεπτομερώς.
Βήμα 1: Αποθήκευση της Επιλογής του Χρήστη
Το localStorage : Η μόνιμη μνήμη σας
Le localStorage`είναι μια API Web Storage που επιτρέπει την αποθήκευση ζευγών κλειδί-τιμή στον περιηγητή με τρόπο διαρκή. Αντίθετα με τα cookie, τα δεδομένα του`localStorage:
-
Ποτέ δεν σταλούν αυτόματα στον διακομιστή
-
Έχουν μεγαλύτερη χωρητικότητα αποθήκευσης (συνήθως 5-10 MB)
-
Δεν έχουν ημερομηνία λήξης (διαμένουν μέχρι ρητή διαγραφή)
-
Είναι περιορισμένες στο πρωτόκολλο και το domain (Same-Origin Policy)
Για την περίπτωση χρήσης μας, το`localStorage`είναι τέλειο γιατί :
-
Δεν χρειαζόμαστε να μοιραστούμε αυτή την πληροφορία με τον διακομιστή
-
θέλουμε η προτίμηση να παραμένει αόριστα
-
Το απαιτούμενο μέγεθος αποθήκευσης είναι ελάχιστο (λίγα byte)
Υλοποίηση του Backup του θέματος
Αυτό είναι ο τρόπος με τον οποίο αποθηκεύουμε την επιλογή του χρήστη όταν αλλάζει θέμα:
// Fonction complète pour changer le thème
function setTheme(newTheme) {
// Validation de l'entrée
if (!['light', 'dark', 'auto'].includes(newTheme)) {
console.error('Thème invalide:', newTheme);
return;
}
try {
// Sauvegarde dans localStorage
localStorage.setItem('preferred-theme', newTheme);
// Application immédiate dans le DOM
document.documentElement.setAttribute('data-bs-theme', newTheme);
// Dispatch d'un événement personnalisé pour notifier d'autres composants
window.dispatchEvent(new CustomEvent('theme-changed', {
detail: { theme: newTheme }
}));
console.log('Thème changé:', newTheme);
} catch (error) {
console.error('Erreur lors de la sauvegarde du thème:', error);
// Fallback : on applique quand même le thème visuellement
document.documentElement.setAttribute('data-bs-theme', newTheme);
}
}
// Exemple d'utilisation avec un bouton
document.getElementById('theme-toggle').addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-bs-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
});
Διαχειριση περιπτωσων σφαλμάτων
Είναι ζωτικής σημασίας να διαχειρίζεστε τις περιπτώσεις όπου το`localStorage`Δεν είναι διαθέσιμο ή προσβάσιμο. Πολλά σενάρια μπορεί να εμποδίσουν την πρόσβαση στο`localStorage`:
απαιjí ιδιωτική περιήγησηSafari σε λειτουργία ανώνυμης περιήγησης προκαλεί μια εξαίρεση`QuotaExceededError`Κατά τη διάρκεια των προσπαθειών εγγραφής στο`localStorage`.
Ρυθμίσεις ιδιωτικότητας: μερικοί φυλλομετρητές ή επεκτάσεις ιδιωτικότητας μπορούν να μπλοκάρουν τη πρόσβαση στο`localStorage`.
Περιορισμοί τομέα : Le `localStorage`δεν είναι διαθέσιμος στο πρωτόκολλο`file://`σε ορισμένους περιηγητές.
Χώρος αποθήκευσης γεμάτοςΑν και σπάνιος, ο χώρος αποθήκευσης μπορεί να είναι πλήρως γεμάτος.
Για_auto ο κώδικάς μας χρησιμοποιεί ένα μπλοκ`try…catch`για να διαχειρίζεται αυτές τις περιπτώσεις ευγενώς, συνεχίζοντας να προσφέρει τη λειτουργία αλλαγής θέματος ακόμα και όταν η διατήρηση δεν είναι διαθέσιμη.
Προχωρημένες Στρατηγικές Διαμονής
Για πιο εξελιγμένες εφαρμογές, μπορείτε να εξετάσετε πρόσθετες στρατηγικές :
Συγχρονισμός διακομιστή (προαιρετικό)
async function setTheme(newTheme) {
// Sauvegarde locale immédiate
localStorage.setItem('preferred-theme', newTheme);
document.documentElement.setAttribute('data-bs-theme', newTheme);
// Synchronisation serveur en arrière-plan (si l'utilisateur est connecté)
if (userIsAuthenticated()) {
try {
await fetch('/api/user/preferences', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme: newTheme })
});
} catch (error) {
console.warn('Échec de la synchronisation serveur:', error);
// L'échec n'est pas critique car la préférence est déjà sauvegardée localement
}
}
}
Αυτή η προσέγγιση επιτρέπει τη synchronism των προτιμήσεων μεταξύ συσκευών για τους συνδεδεμένους χρήστες, διατηρώντας παράλληλα την άμεση τοπική ανταπόκριση.
Βήμα 2 : Το σενάριο προφόρτωσης στο `<head>
Η Καρδιά της Λύσης
Αυτό είναι όπου η μαγία λειτουργεί πραγματικά. Θα τοποθετήσουμε ένα μικρό σενάριο.γραμμικόαμέσως στο`<head>`, πριν από όλα τα ετικέτα μας`<link>`de φύλλα στυλ. Αυτό το σενάριο είναι προσεκτικά ελαφρύ, αυτόνομο και σχεδιασμένο ώστε να εκτελείται όσο το δυνατόν γρήγορα.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mon Site Incroyable</title>
<!-- ==========================================
NOTRE SCRIPT MAGIQUE DE PRÉ-CHARGEMENT
Ce script DOIT être le premier élément
dans le <head> après les meta tags
========================================== -->
<script>
// IIFE pour ne pas polluer le scope global
(function() {
'use strict';
try {
// Lecture de la préférence sauvegardée
const savedTheme = localStorage.getItem('preferred-theme');
// Si une préférence existe, on l'applique immédiatement
if (savedTheme) {
document.documentElement.setAttribute('data-bs-theme', savedTheme);
}
// Optionnel : Détecter la préférence système si aucune sauvegarde
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark');
}
// Sinon, le thème par défaut du CSS sera utilisé (généralement 'light')
} catch (error) {
// En cas d'erreur (localStorage bloqué, etc.), on log discrètement
// et on laisse le thème par défaut s'appliquer
console.warn('Impossible de charger la préférence de thème:', error);
}
})();
</script>
<!-- FIN DU SCRIPT MAGIQUE -->
<!-- Les feuilles de style sont chargées APRÈS le script -->
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/styles.css">
<!-- Autres ressources du head -->
<link rel="icon" href="favicon.ico">
</head>
<body>
<!-- Contenu de la page -->
</body>
</html>
Ανατομή του Script : Κάθε Γραμμή Σημανίζει
Αναλύουμε αυτό το σενάριο γραμμή ανά γραμμή για να καταλάβουμε κάθε απόφαση σχεδιασμού :
Η IIFE (Έκφραση συνάρτησης που καλείται αμέσως)
(function() {
// ...
})();
Αυτή η δομή δημιουργεί μια συνάρτηση που εκτελείται αμέσως. Γιατί; Για να απομονώσουμε τις μεταβλητές μας σε ένα τοπικό scope και να αποφύγουμε τη μολυνση του global scope. Ακόμα και αν χρησιμοποιούμε μόνο`const`(που دارد εμβέλεια μπλοκ), το IIFE είναι μια καλή πρακτική που κάνει τους σκοπούς μας σαφείς και προστατεύει από πιθανούς συγκρούσεις ονομάτων.
Λειτουργία ΣΤΡΙΚΤ
'use strict';
Αυτή η οδηγία ενεργοποιεί τη στενή λειτουργία του JavaScript,που: - Απαγορεύει τη χρήση μη δηλωμένων μεταβλητών - Παραγεί σφάλματα για τις επικίνδυνες λειτουργίες - Βελτιώνει τις αποδόσεις σε ορισμένα JavaScript μηχανές
Για ένα κρίσιμο σενάριο όπως αυτό, θέλουμε τη μέγιστη ασφάλεια.
Το μπλοκ try…catch
try {
// Code principal
} catch (error) {
console.warn('Impossible de charger la préférence de thème:', error);
}
Αυτό το μπλοκ είναι απόλυτα ζωτικό. Εγγυάται ότι αν κάτι πάει στραβά (το localStorage είναι κλεισμένο, improbable syntax error, κ.λπ.), το script μας δεν θαμπολέει τη φόρτωση όλης της σελίδας. Η χρήση του`console.warn`αντί να`console.error`δεικνύει ότι είναι ένα μη κρίσιμο πρόβλημα.
Η ανάγνωση του localStorage
const savedTheme = localStorage.getItem('preferred-theme');
Αυτή η γραμμή μπορεί να πετάξει μια εξαίρεση σε ορισμένα πλαίσια (συμμόρια ιδιωτικής περιήγησης του Safari). Επομένως, βρίσκεται σε ένα μπλοκ try…catch.
Η συνθηματική εφαρμογή
if (savedTheme) {
document.documentElement.setAttribute('data-bs-theme', savedTheme);
}
Εφαρμόζουμε το θέμα μόνο αν βρούμε ένα αποθηκευμένο. Αλλιώς, επιτρέπουμε στο CSS να χρησιμοποιήσει το προεπιλεγμένο του θέμα. Αυτή η προσέγγιση είναι πιο αξιόπιστη από μια προκαθορισμένη τιμή που είναι σκληρά κωδικοποιημένη στην JavaScript.
Ανίχνευση της προτιμήσης του συστήματος (Bonus)
Μια προαιρετική αλλά ελεγαντή βελτίωση αποτελεί το εντοπισμός της προτίμησης θέματος του λειτουργικού συστήματος του χρήστη, εάν δεν έχει ακόμη κάνει μια ρητή επιλογή στην εφαρμογή σας :
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark');
}
Αυτή η λειτουργία χρησιμοποιεί την Media Query`prefers-color-scheme`Για να ερωτήσετε το σύστημα. Στο macOS, Windows 10+, iOS, και Android σύγχρονο, αυτή η ερώτηση επιστρέφει την προτίμηση του συστήματος του χρήστη.
Πλεονεκτήματα : - Προσωποποιημένη εμπειρία από την πρώτη επίσκεψη - Συμφωνία με το περιβάλλον του συστήματος του χρήστη - Δεν απαιτείται αποθήκευση για την πρώτη επίσκεψη
Σκέψεις : - Όχι όλοι οι περιηγητές υποστηρίζουν αυτή τη λειτουργία (όμως η υποστήριξη είναι εξαιρετική από το 2020) - Η επαλήθευση`window.matchMedia`Εγγυάται τη συμβατότητα - Ο χρήστης μπορεί πάντα να την αντικαταστήσει
Απόδοση: Γιατί αυτό το Script είναι γρήγορο
Το script προφορτύλισης μας είναι σχεδιασμένο να είναι εξαιρετικά γρήγορο:
ελάχιστο μέγεθοςΠερίπου 300 byte μη μειωμένα, 200 byte μειωμένα. Είναι αβασανιστικό σε σύγκριση με οποιαδήποτε εικόνα ή βιβλιοθήκη JavaScript.
InlineΔεν απαιτείται πρόσθετη HTTP αίτηση. Ο κώδικας είναι στο HTML, άρα είναι αμέσως διαθέσιμος.
Απλές συγχρονιστικές λειτουργίεςΑνάγνωση ενός κλειδιού στο localStorage (πολύ γρήγορη λειτουργία) και τροποποίηση ενός DOM attribute (ενσωματωμένη λειτουργία του περιηγητή).
Καμία εξάρτησηΚαμία πλαίσιο, καμία βιβλιοθήκη, απλώς καθαρός JavaScript. Χωρίς χρόνο εκκίνησης, χωρίς ανάλυση εξαρτήσεων.
Μία εκτέλεσηΑυτό το σενάριο εκτελείται μόνο μία φορά κατά το φόρτωμα. Δεν υπάρχουν ακροατείς συμβάντων, δεν υπάρχουν βρόχοι, δεν υπάρχουν πολύπλοκοι υπολογισμοί.
Στη πράξη, σε σύγχρονο υλικό, αυτός ο script εκτελείται σε λιγότερο από 1 χιλιοστό του δευτέρου, ένας άπτοπτος χρόνος που δεν έχει καμία επίπτωση στην απόδοση φόρτωσης της σελίδας.
Βέλτιστη τοποθέτηση στο `<head>
Η σειρά των στοιχείων στο`<head>`Είναι σημαντικό. Εδώ είναι η προτεινόμενη σειρά:
<head>
<!-- 1. Métadonnées critiques -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 2. Notre script de pré-chargement (IMMÉDIATEMENT après les meta) -->
<script>
(function() { /* notre code */ })();
</script>
<!-- 3. Titre de la page -->
<title>Mon Site</title>
<!-- 4. Feuilles de style -->
<link rel="stylesheet" href="styles.css">
<!-- 5. Autres ressources (fonts, favicons, etc.) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="icon" href="favicon.ico">
<!-- 6. Autres scripts avec defer ou async -->
<script src="app.js" defer></script>
</head>
Αυτή η εντολή εγγυάει ότι:
-
Το charset ορίζεται πριν από κάθε επεξεργασία κειμένου
-
Το script μας εκτελείται πριν τη φόρτωση των CSS
-
Τα CSS φορτώνονται στη συνέχεια και εφαρμόζουν apeυθείας το σωστό θέμα
-
Οι άλλοι ακρίσιμοι πόροι φορτίζονται τελευταίοι
Βήμα 3 : Η Ισχύς των Επιλογέων Χαρακτηριστικών CSS
Το Σύστημα Θεμάτων του Bootstrap 5
Το Bootstrap 5 εισήγαγε ένα ελεγκτό σύστημα διαχείρισης θεμάτων βασισμένο στα CSS custom properties (μεταβλητές CSS) και τους επιλογείς χαρακτηριστικών. Αυτό το σύστημα χρησιμοποιεί το χαρακτηριστικό`data-bs-theme`στο στοιχείο`<html>`για να καθορίσει ποιο σύνολο μεταβλητών χρώματος να εφαρμόσετε
Η ομορφιά αυτού του συστήματος βρίσκεται στην απλότητά του: αντί για το να φορτώνουμε διαφορετικά φύλλα στυλ ή να εναλλάσσομαστε κλάσεις σε χιλιάδες στοιχεία, αλλάζουμε απλώς ένα χαρακτηριστικό σε ένα μόνο στοιχείο, και το CSS κάνει το υπόλοιπο χάρη στη διαδοχή.
Δομή CSS για ένα Σύστημα Θέματος
Εδώ είναι μια πλήρης δομή CSS για την υλοποίηση ενός ανθεκτικού συστήματος θεμάτων :
/**
* SYSTÈME DE THÈME COMPLET
* Utilise les Custom Properties CSS pour une maintenance facile
*/
/* ============================================
THÈME PAR DÉFAUT (LIGHT)
Défini sur :root pour être le fallback
============================================ */
:root {
/* Couleurs de base */
--color-primary: #0d6efd;
--color-secondary: #6c757d;
--color-success: #198754;
--color-danger: #dc3545;
--color-warning: #ffc107;
--color-info: #0dcaf0;
/* Couleurs de fond */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
/* Couleurs de texte */
--text-primary: #212529;
--text-secondary: #6c757d;
--text-tertiary: #adb5bd;
/* Couleurs de bordure */
--border-color: #dee2e6;
--border-color-subtle: #e9ecef;
/* Couleurs d'ombre */
--shadow-sm: rgba(0, 0, 0, 0.075);
--shadow-md: rgba(0, 0, 0, 0.15);
--shadow-lg: rgba(0, 0, 0, 0.25