<!--
  canonical-url: https://www.avonture.be/fr/blog/docusaurus-check-images
  published:     2026-01-26
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# Quelques vérifications sur les images de votre site Docusaurus

> Un script Python pour vérifier le lazy loading et la taille des images de votre site Docusaurus avec Playwright et BeautifulSoup.

---
![Quelques vérifications sur les images de votre site Docusaurus](/img/v2/check_images.webp)

**TL;DR**

Cet article explique comment imposer le lazy loading sur un blog Docusaurus en surchargeant le composant image MDX par défaut pour injecter automatiquement l'attribut loading. Il détaille ensuite comment valider ce comportement à l'aide d'un script Python et Playwright containerisé qui simule le scroll d'un utilisateur pour contrôler les attributs des images et la taille des fichiers. Ce workflow garantit que les bonnes pratiques de performance sont appliquées et vérifiées de manière cohérente sur tout le contenu du blog.

Vous me connaissez suffisamment maintenant : la performance me tient à cœur. Pour plein de bonnes raisons, mais surtout parce que c'est amusant d'optimiser les choses et de voir les résultats immédiatement. Et c'est aussi un joli défi !

Mon blog est devenu assez gros (plus de 200 articles) et je voulais m'assurer que toutes les images sont bien chargées en lazy loading — mais comment en être sûr ?

Le lazy load, c'est quoi ? Quand vous chargez une image en lazy loading, vous dites au navigateur de ne la charger qu'au moment où elle est sur le point d'entrer dans le viewport (c'est-à-dire quand l'utilisateur scrolle à proximité). Donc, au lieu de charger toutes les images d'un coup au chargement de la page, seules les images immédiatement visibles sont chargées en premier. Cela réduit le temps de chargement initial et économise de la bande passante, en particulier sur les pages comportant beaucoup d'images.

Dans cet article, je partage comment j'ai imposé le lazy loading sur ce blog Docusaurus et, surtout, comment j'ai automatisé la vérification de ce comportement avec un script Python.

## Lancer la vérification {#running-the-check}

Une fois le script (montré plus bas) et son container en place, auditer chaque image du site tient en une seule commande :

```bash
$ docker run -it --rm -v .:/app -w /app --entrypoint /bin/sh mcr.microsoft.com/playwright/python:v1.61.0-jammy -c "pip install --root-user-action=ignore beautifulsoup4 pillow playwright requests >/dev/null && python check-images.py"
```

J'obtiens dans la console un rapport indiquant les images auxquelles il manque l'attribut `loading="lazy"`, surtout si elles sont volumineuses.

Par exemple :

![Exemple de sortie](./images/output.webp)

À vous, maintenant, d'agir sur les résultats ! Sur mon propre blog, j'ai ajouté quelques attributs manquants et optimisé des images trop lourdes. J'ai aussi revu mes composants React pour m'assurer qu'ils incluent le lazy loading là où c'est pertinent.

## Étape 1 : imposer le lazy loading dans Docusaurus {#step-1-enforcing-lazy-loading-in-docusaurus}

Par défaut, quand vous écrivez la syntaxe Markdown standard pour une image `![Alt text](image.webp)`, Docusaurus génère une balise HTML `<img>` classique. Les navigateurs modernes supportent l'attribut `loading="lazy"`, mais le framework ne l'ajoute pas toujours automatiquement.

Pour garantir que **chaque image** de mon blog est chargée en lazy loading (et décodée de manière asynchrone), j'ai surchargé le composant `img` par défaut de Docusaurus. J'ai déjà raconté cette histoire deux fois : sous l'angle du composant dans [Change how Docusaurus will create img tags](/blog/docusaurus-override-img) et sous l'angle du plugin dans [Overrides the generation of img tags with Docusaurus](/blog/docusaurus-lazy-loading).

Cela se fait dans `src/theme/MDXComponents.js`. En englobant le composant image par défaut, je peux injecter les attributs dont j'ai besoin :

**src/theme/MDXComponents.js**

```javascript
import MDXComponents from "@theme-original/MDXComponents";

export default {
  // Reusing the default mapping
  ...MDXComponents,

  // Override the img tag
  img: (props) => {
    const { loading, decoding, className, style, ...rest } = props;

    return (
      <img
        {...rest}
        // Force lazy loading if not specified
        loading={loading || "lazy"}
        // Decode asynchronously to avoid blocking the main thread
        decoding={decoding || "async"}
      />
    );
  },
};
```

Avec ce simple changement, chaque image générée depuis du Markdown possède désormais `loading="lazy"`.

## Étape 2 : faire confiance, mais vérifier {#step-2-trust-but-verify}

Écrire le code est une chose ; s'assurer que ça fonctionne sur des centaines d'articles en est une autre. Je voulais être sûr que :

1. Les images ont bien l'attribut `loading="lazy"`.
2. Les grandes images sont correctement gérées.
3. Le comportement persiste même après une mise à jour du site.

Inspecter les éléments à la main dans les DevTools de Chrome est fastidieux. J'ai donc écrit un **script Python** pour automatiser tout ça.

### Le défi : le scroll {#the-challenge-scrolling}

L'analyse statique (télécharger simplement le HTML) ne suffit pas. Certaines implémentations de lazy loading (ou certains processus d'hydratation) peuvent dépendre du fait que l'utilisateur scrolle réellement la page.

Pour simuler un vrai utilisateur, j'ai utilisé **Playwright**. Il me permet de lancer un navigateur headless, de rendre la page et de scroller jusqu'en bas par programmation, ce qui déclenche les mécanismes de lazy loading.

### Le script {#the-script}

Le script effectue les actions suivantes :

1. **Parcourt** une liste d'URLs (mes articles de blog).
2. **Scrolle** plusieurs fois vers le bas de la page (comme un lecteur).
3. **Parse** le DOM avec `BeautifulSoup`.
4. **Inspecte** chaque balise `<img>` pour vérifier la présence de `loading="lazy"`.
5. **Contrôle** les dimensions des images avec `Pillow` pour signaler les images volumineuses qui devraient absolument être en lazy loading.

**check-images.py**

```python
# type: ignore
# cspell:ignore networkidle

"""
Advanced Image Audit Script for React Sites

This script uses Playwright to render a React website in a headless Chromium browser,
scrolls the page to trigger lazy loading, and audits all <img> tags for:
- Lazy loading usage
- Image dimensions (width x height)
- Format (e.g., WebP, PNG, JPEG)
- Compression (file size)
- Presence of width/height attributes
- Duplicate image sources

Usage in Docker:

    docker run -it --rm \
        -v ${PWD}:/app \
        -w /app \
        --entrypoint /bin/sh \
        mcr.microsoft.com/playwright/python:v1.57.0-jammy \
        -c "pip install --root-user-action=ignore beautifulsoup4 pillow playwright requests >/dev/null && python lazy-load.py"

Notes:
- The script ignores SSL errors (useful for local dev or self-signed certs)
- It scrolls the page multiple times to ensure lazy-loaded images appear in the DOM
- Requires 'beautifulsoup4' and 'playwright' Python packages
"""

from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
import time
import requests
from PIL import Image
from io import BytesIO
from urllib.parse import urljoin
import urllib3

# List of pages to check
urls = [
    "https://host.docker.internal:3000/",
    # "https://host.docker.internal:3000/about",
    # "https://host.docker.internal:3000/blog",
    # "https://host.docker.internal:3000/blog/archive",
    # add more pages here
]

# Disable SSL warnings for unverified HTTPS requests
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def get_image_info(src, base_url):
    """Returns image size (width, height), format, and file size in KB"""
    if not src or src.startswith("data:image/"):
        return None  # Skip inline base64 images

    full_url = urljoin(base_url, src)

    try:
        response = requests.get(full_url, stream=True, timeout=5, verify=False)
        response.raise_for_status()
        img = Image.open(BytesIO(response.content))
        size_kb = len(response.content) / 1024
        return {
            "width": img.width,
            "height": img.height,
            "format": img.format,
            "size_kb": round(size_kb, 1),
        }
    except Exception as e:
        print(f"⚠️ Could not get info for {full_url}: {e}")
        return None

# Scrolling configuration
NUM_SCROLLS = 10       # Number of scrolls to perform to trigger lazy-loaded images
SCROLL_HEIGHT = 1000   # Pixels per scroll
WAIT_PER_SCROLL = 1    # Seconds to wait after each scroll

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)

    for url in urls:
        print(f"\n🔎 Checking page: {url}")
        page = browser.new_page(ignore_https_errors=True)

        # Navigate and wait for network to be idle
        page.goto(url)
        page.wait_for_load_state("networkidle")

        # Scroll to trigger lazy loading
        for _ in range(NUM_SCROLLS):
            page.mouse.wheel(0, SCROLL_HEIGHT)
            time.sleep(WAIT_PER_SCROLL)

        # Get the rendered HTML
        html = page.content()
        page.close()

        # Parse HTML
        soup = BeautifulSoup(html, "html.parser")
        images = soup.find_all("img")

        print(f"{len(images)} images found on {url}\n")

        lazy_images = []
        non_lazy_images = []
        warnings = []

        for img in images:
            src = img.get("src")
            if not src:
                warnings.append("[missing src] image tag")
                continue

            # Check lazy loading
            is_lazy = img.get("loading") == "lazy"

            # Check width/height attributes
            has_dimensions = img.get("width") and img.get("height")
            if not has_dimensions:
                warnings.append(f"{src} missing width/height attributes")

            # Get image info
            info = get_image_info(src, url)
            if info is None:
                continue  # Skip if image couldn't be loaded

            width = info["width"]
            height = info["height"]
            format = info["format"]
            size_kb = info["size_kb"]

            # Check format
            if format.lower() in ["jpeg", "jpg", "png"]:
                warnings.append(f"{src} is {format}, consider converting to WebP")

            # Check compression
            if size_kb > 200:
                warnings.append(f"{src} is {size_kb} KB — consider compressing")

            # Classify image
            if is_lazy:
                lazy_images.append(f"{src} ✅ lazy ({width}x{height}, {format}, {size_kb} KB)")
            else:
                non_lazy_images.append(f"{src} ❌ not lazy ({width}x{height}, {format}, {size_kb} KB)")

        # Print results
        for entry in non_lazy_images:
            print(entry)
        for entry in warnings:
            print(f"⚠️ Warning: {entry}")

        print(f"\n✅ Lazy images: {len(lazy_images)}")
        print(f"❌ Non-lazy images: {len(non_lazy_images)}")
        print(f"⚠️ Warnings: {len(warnings)}")

    browser.close()
```

Vous me connaissez très bien maintenant : j'aime containeriser les choses. Je n'utilisais donc pas Python ni Playwright directement sur ma machine hôte, mais dans un container Docker. Ce container monte le répertoire courant, installe les packages nécessaires et exécute `check-images.py` — exactement la commande montrée en haut de cet article.

> **Note:**
>
> Ce script a depuis été retiré de ma boîte à outils et n'est conservé ici qu'à titre de référence ; le code ci-dessus fonctionne toujours comme décrit.

### Vérifications supplémentaires {#additional-checks}

Le script réalise aussi ces contrôles :

* Vérifier si le format de l'image est WebP ; sinon, il émet un avertissement.
* Vérifier si le poids de l'image dépasse un certain seuil (200 Ko) ; si oui, il émet un avertissement.
* Vérifier si les attributs de hauteur et de largeur sont définis ; sinon, il émet un avertissement.

Vous pouvez facilement adapter ces vérifications dans le script selon vos besoins. Et ajouter les vôtres !

## Conclusion {#conclusion}

L'optimisation des performances est un chemin, pas une destination. En combinant une surcharge globale dans Docusaurus avec un script de validation automatisé, je peux écrire tranquillement, en sachant que mes images ne ralentiront pas l'expérience de mes lecteurs.

Si le script complet vous intéresse, vous le trouverez dans le dossier `.scripts` de mon repository !
