Aller au contenu

Chapitre 33 — Optimisation SCSS

Introduction

L'optimisation du code SCSS passe par la compilation, la compression, la minification, l'analyse des performances et la bonne gestion des dépendances. Ce chapitre couvre toutes les techniques pour produire un CSS final performant.


Options de compilation

Compilation standard

# Compilation basique
sass input.scss output.css

# Compilation avec style compressé
sass --style=compressed input.scss output.css

# Compilation watch (surveillance)
sass --watch input.scss:output.css

# Compilation du dossier entier
sass --watch src/:dist/

Options CLI

# Toutes les options
sass --help

# Options principales :
# --style=expanded|compressed|compact
# --source-map
# --no-source-map
# --watch
# --update
# --load-path=PATH
# --no-charset
# --error-css
# --quiet-deps
# --silence-deprecation=DEP

Styles de sortie

Comparaison des styles

// input.scss
$color: #3498db;
$size: 16px;

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 24px;

  &__title {
    color: $color;
    font-size: $size * 2;
    font-weight: 700;
  }

  &__text {
    color: darken($color, 20%);
    line-height: 1.5;
  }
}

Expanded (développé)

/* output: expanded */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 24px;
}

.container__title {
  color: #3498db;
  font-size: 32px;
  font-weight: 700;
}

.container__text {
  color: #2980b9;
  line-height: 1.5;
}

Compressed (compressé)

/* output: compressed */
.container{max-width:1200px;margin:0 auto;padding:24px}.container__title{color:#3498db;font-size:32px;font-weight:700}.container__text{color:#2980b9;line-height:1.5}

Compact

/* output: compact */
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
.container__title { color: #3498db; font-size: 32px; font-weight: 700; }
.container__text { color: #2980b9; line-height: 1.5; }

Compression avancée

Suppression des caractères inutiles

// Les espaces et sauts de ligne inutiles
$color: #3498db;

// Avant compilation :
.element {
  margin: 0 px;
  padding: 16 px;
  background-color: #3498db;
  background-color: rgba(52, 152, 219, 1);
  font-weight: 400;
  opacity: 1;
  display: block;
}

// Après compression :
.element {
  margin: 0px;
  padding: 16px;
  background-color: #3498db;
  font-weight: 400;
  opacity: 1;
  display: block;
}

// Optimisation manuelle :
.element {
  margin: 0;        // 0px  0
  padding: 1rem;    // 16px  1rem (si design system)
  background-color: #3498db;  // rgba inutile quand alpha=1
  font-weight: 400;          // supprimable si c'est la valeur par défaut
  opacity: 1;                // supprimable si c'est la valeur par défaut
  display: block;            // supprimable si c'est la seule option
}

Règles d'optimisation

// 1. Éviter les valeurs redondantes
.bad {
  font-weight: bold;           // OK
  font-weight: 700;            // OK (identique)
  font-weight: bolder;         // Différent ! Attention
}

// 2. Utiliser les raccourcis
.bad {
  margin-top: 10px;
  margin-right: 10px;
  margin-bottom: 10px;
  margin-left: 10px;
}

.good {
  margin: 10px;               // Raccourci
}

// 3. Éviter les couleurs calculées inutilement
.bad {
  color: darken(#3498db, 0%);  // Inutile, retourne la même couleur
}

.good {
  color: #3498db;
}

// 4. Éviter les opérations inutiles
.bad {
  width: 100% * 1;            // Inutile
  font-size: 16px + 0px;     // Inutile
}

.good {
  width: 100%;
  font-size: 16px;
}

Source Maps

Qu'est-ce qu'un source map ?

Un source map est un fichier .map qui permet de mapper le CSS compilé vers le SCSS source. Indispensable pour le debugging.

Génération

# Avec source map (par défaut)
sass input.scss output.css --source-map

# Sans source map
sass input.scss output.css --no-source-map

# Source map intégré (inline)
sass input.scss output.css --source-map=inline

# Source map avec chemin personnalisé
sass input.scss output.css --source-map=maps/output.css.map

Structure du source map

{
  "version": 3,
  "file": "output.css",
  "sourceRoot": "",
  "sources": [
    "../src/scss/abstracts/_variables.scss",
    "../src/scss/base/_reset.scss",
    "../src/scss/components/_buttons.scss"
  ],
  "names": [],
  "mappings": "AAAA,SAAS,..."
}

Configuration dans un projet

// gulpfile.js (Gulp)
const sass = require('gulp-sass')(require('sass'));

gulp.task('sass', function () {
  return gulp.src('src/scss/**/*.scss')
    .pipe(sass({
      outputStyle: 'compressed',
      sourceMap: true,
      sourceMapContents: true,
      sourceMapEmbed: false,
    }).on('error', sass.logError))
    .pipe(gulp.dest('dist/css'));
});

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.scss$/,
      use: [
        'style-loader',
        'css-loader',
        {
          loader: 'sass-loader',
          options: {
            sourceMap: true,
          },
        },
      ],
    }],
  },
};

Visualiser les source maps

# Ouvrir dans Chrome DevTools
# 1. Ouvrir l'onglet Sources
# 2. Chercher le fichier SCSS dans le tree
# 3. Cliquer sur une ligne pour voir le CSS correspondant

Minification avancée

PostCSS + CSSNano

// postcss.config.js
module.exports = {
  plugins: [
    require('cssnano')({
      preset: ['advanced', {
        discardComments: {
          removeAll: true,
        },
        normalizeUrl: true,
        minifyFontValues: true,
        minifyGradients: true,
        mergeRules: true,
        mergeLonghand: true,
        reduceIdents: true,
        colormin: true,
      }],
    }),
  ],
};

PurgeCSS

// purgecss.config.js
module.exports = {
  content: [
    './src/**/*.html',
    './src/**/*.js',
    './src/**/*.jsx',
    './src/**/*.vue',
    './src/**/*.svelte',
  ],
  css: ['./dist/css/**/*.css'],
  defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
  safelist: [
    'active',
    'show',
    'hidden',
    /^modal-/,
    /^tooltip-/,
    /^dropdown-/,
  ],
};

Configuration SCSS pour PurgeCSS

// Les classes dynamiques doivent être dans des commentaires spéciaux
// ou dans un fichier séparé pour PurgeCSS

// classes-purge.scss
// @layer utilities {
//   .dynamic-class-1 { color: red; }
//   .dynamic-class-2 { color: blue; }
// }

Performance SCSS

Éviter la récursion profonde

// MAUVAIS : récursion profonde
@for $i from 1 through 100 {
  .item-#{$i} {
    width: $i * 1%;
    @for $j from 1 through 50 {
      .sub-#{$j} {
        margin-left: $j * 2px;
      }
    }
  }
}
// Résultat : potentiellement des milliers de règles

// BON : limiter la complexité
@for $i from 1 through 12 {
  .col-#{$i} {
    grid-column: span $i;
  }
}

Limiter les opérations coûteuses

// MAUVAIS : opérations dans des boucles
@for $i from 1 through 1000 {
  .item-#{$i} {
    color: darken($color-primary, $i * 0.1%);
  }
}

// BON : valeurs pré-calculées ou map
$color-shades: (
  100: darken($color-primary, 5%),
  200: darken($color-primary, 10%),
  300: darken($color-primary, 15%),
  400: darken($color-primary, 20%),
  500: darken($color-primary, 25%),
);

@each $shade, $color in $color-shades {
  .text-primary-#{$shade} {
    color: $color;
  }
}

Éviter les @import en boucle

// MAUVAIS : importer dans une boucle
@for $i from 1 through 10 {
  @import 'components/component-#{$i}';
}

// BON : importer chaque fichier séparément
@import 'components/component-1';
@import 'components/component-2';
// ...
@import 'components/component-10';

Optimiser les maps

// MAUVAIS : maps imbriquées profondément
$map: (
  level1: (
    level2: (
      level3: (
        level4: (
          value: #333,
        ),
      ),
    ),
  ),
);

// BON : maps plats quand possible
$colors: (
  primary: #3498db,
  secondary: #2ecc71,
  danger: #e74c3c,
);

// Si nécessaire, max 2 niveaux d'imbrication
$theme-light: (
  bg: #ffffff,
  text: #333333,
  border: #e0e0e0,
);

Analyse du CSS généré

Outils d'analyse

# Avec stylestats
npx stylestats dist/css/main.css

# Avec pack-size
npx pack-size dist/css/main.css

# Avec CSS Analyzer
npx css-analyzer dist/css/main.css

Analyse manuelle

// Ajouter des commentaires de debug
// # DEBUG: start - total rules count
// Le CSS généré aura ces commentaires
// # DEBUG: end - total rules count

// ou en SCSS :
// stylelint-disable-next-line

Métriques à surveiller

# Taille totale
wc -c dist/css/main.css

# Nombre de lignes
wc -l dist/css/main.css

# Nombre de sélecteurs
grep -o '[^{]*{' dist/css/main.css | wc -l

# Nombre de media queries
grep -c '@media' dist/css/main.css

# Couleurs uniques
grep -oE '#[0-9a-fA-F]{3,6}' dist/css/main.css | sort -u | wc -l

Arbre de dépendances

Gestion des @use et @forward

// SCSS moderne : @use et @forward

// abstracts/_index.scss
@forward 'variables';
@forward 'functions';
@forward 'mixins';

// components/_index.scss
@forward 'buttons';
@forward 'cards';
@forward 'forms';

// main.scss
@use 'abstracts';
@use 'components';

// ou avec alias
@use 'abstracts' as a;
@use 'components' as c;

.container {
  @include a.flex-center;
  color: a.$color-primary;
}

.btn {
  @include c.button-styles;
}

Éviter les conflits de noms

// Si deux modules exportent la même variable
@use 'abstracts/variables';
@use 'theme/variables';

// Conflit ! Solution : alias
@use 'abstracts/variables' as abs;
@use 'theme/variables' as theme;

.element {
  color: abs.$color-primary;
  background: theme.$bg-color;
}

Arbre de dépendances propre

sass/
├── abstracts/
│   ├── _index.scss          ← forward tout
│   ├── _variables.scss
│   ├── _functions.scss
│   └── _mixins.scss
├── base/
│   ├── _index.scss
│   ├── _reset.scss
│   └── _typography.scss
├── components/
│   ├── _index.scss
│   ├── _buttons.scss        ← @use '../abstracts'
│   ├── _cards.scss          ← @use '../abstracts'
│   └── _forms.scss          ← @use '../abstracts'
└── main.scss                ← @use 'abstracts', 'base', 'components'

Bundle CSS analysis

Rapport de compilation

# Compiler avec verbose
sass --verbose input.scss output.css

# Compiler avec stats
sass --style=compressed input.scss output.css 2>&1 | grep -i "time\|rules\|size"

Visualiser la taille par section

// Ajouter des commentaires de section pour l'analyse
// === ABSTRACTS (0 CSS) ===
// === BASE (X rules) ===
// === COMPONENTS (Y rules) ===
// === UTILITIES (Z rules) ===

Outil d'audit

// audit.scss - fichier d'audit à compiler séparément
@debug "Nombre de variables: #{length($all-variables)}";
@debug "Nombre de mixins: #{length($all-mixins)}";
@debug "Nombre de composants: #{length($all-components)}";

// Compter les sélecteurs générés
$selector-count: 0;
@each $component in $components {
  // ...
}

Bonnes pratiques d'optimisation

1. Compilation

# Production : toujours compressé
sass --style=compressed src/main.scss dist/css/main.css

# Développement : expanded avec source maps
sass --watch --style=expanded --source-map src/:dist/

2. Structure des imports

// Éviter les imports profonds
@import 'a/b/c/d/variable';    // MAUVAIS

// Préférer les imports plats avec index
@use 'abstracts';               // BON

3. Variables

// Éviter les variables inutilisées
$unused-color: red;    // Supprimez-la
$used-color: blue;     // Gardez-la

// Utiliser !default pour la configurabilité
$color-primary: #3498db !default;

4. Mixins

// Éviter les mixins qui génèrent beaucoup de CSS
@mixin massive-mixin {
  @for $i from 1 through 100 {
    .item-#{$i} { width: $i * 1%; }
  }
}

// Préférer des mixins ciblés
@mixin responsive-grid($cols) {
  display: grid;
  grid-template-columns: repeat($cols, 1fr);
}

5. Sélecteurs

// Éviter les sélecteurs trop spécifiques
.parent .child .grandchild .item { }  // MAUVAIS

.item { }  // BON (avec BEM)

// ou
.card__item { }  // BEM

Arbre de fichiers optimisé

src/
├── abstracts/
│   ├── _index.scss
│   ├── _variables.scss
│   ├── _functions.scss
│   └── _mixins.scss
├── base/
│   ├── _index.scss
│   ├── _reset.scss
│   └── _typography.scss
├── components/
│   ├── _index.scss
│   ├── _buttons.scss
│   ├── _cards.scss
│   └── _forms.scss
├── utilities/
│   ├── _index.scss
│   └── _generated.scss
└── main.scss

dist/
├── css/
│   ├── main.css           ← Compressé
│   ├── main.css.map       ← Source map
│   └── main.min.css       ← Minifié (PostCSS)
└── maps/
    └── main.css.map

Pipeline de build complet

// gulpfile.js
const { src, dest, watch, series } = require('gulp');
const sass = require('gulp-sass')(require('sass'));
const postcss = require('gulp-postcss');
const cssnano = require('cssnano');
const autoprefixer = require('autoprefixer');
const purgecss = require('@fullhuman/postcss-purgecss');

function compileSass() {
  return src('src/scss/main.scss')
    .pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError))
    .pipe(dest('dist/css'));
}

function optimizeCss() {
  return src('dist/css/main.css')
    .pipe(postcss([
      autoprefixer(),
      cssnano({ preset: 'advanced' }),
    ]))
    .pipe(dest('dist/css'));
}

function purgeUnused() {
  return src('dist/css/main.css')
    .pipe(postcss([
      purgecss({
        content: ['src/**/*.{html,js,jsx,ts,tsx,vue}'],
      }),
    ]))
    .pipe(dest('dist/css'));
}

exports.build = series(compileSass, optimizeCss, purgeUnused);
exports.dev = () => watch('src/scss/**/*.scss', compileSass);

Résumé

L'optimisation SCSS consiste à produire le CSS le plus petit et le plus performant possible. Utilisez --style=compressed en production, activez les source maps en développement, purgez le CSS inutilisé, et surveillez la taille de votre bundle. Une bonne architecture (7-1 ou ITCSS) facilite naturellement l'optimisation.