Essayez sans attendre l'hébergement proposé par WordPress
-15% sur le premier mois avec le code 2025PRESS15AFF

Essayer maintenant

WordPress et les Web Workers : Déporter les traitements lourds côté client

Bon, je vais être honnête avec vous : combien de fois votre admin WordPress s’est figé pendant un import CSV ou lors du traitement d’images lourdes ? Les Web Workers offrent une solution élégante pour déporter ces traitements côté client, sans bloquer l’interface utilisateur. On va voir ensemble comment intégrer cette technologie dans WordPress pour créer des expériences utilisateur bien plus fluides.

Comprendre les Web Workers dans le contexte WordPress

Avant de plonger dans le code, il faut bien comprendre ce qu’on manipule. Les Web Workers, c’est un peu comme avoir un assistant qui travaille en arrière-plan pendant que vous continuez vos tâches principales. Et croyez-moi, dans WordPress, on en a bien besoin !

Qu’est-ce qu’un Web Worker et pourquoi s’en servir ?

Un Web Worker, c’est un script JavaScript qui s’exécute dans un thread séparé du thread principal de votre page. Concrètement, ça veut dire que vous pouvez déporter des calculs lourds sans bloquer l’interface utilisateur.

Pourquoi c’est révolutionnaire pour WordPress ? Bon, je vais être honnête : j’ai longtemps ignoré cette technologie. Jusqu’au jour où j’ai développé un import CSV pour un client avec 50 000 produits WooCommerce. L’interface admin se figeait complètement pendant 30 secondes ! L’utilisateur ne savait plus si ça marchait ou si c’était planté.

Avec un Web Worker, le parsing du CSV et les traitements de données se font en arrière-plan. L’utilisateur peut continuer à naviguer, voir une barre de progression qui se met à jour… Bref, une expérience utilisateur digne de ce nom.

Les limitations du thread principal et l’impact sur l’UX

Le thread principal de JavaScript, c’est un peu comme une route à une seule voie : tout doit passer par là. Quand vous lancez une opération lourde, tout s’arrête. Plus de scroll, plus de clics, plus rien.

Dans WordPress, on rencontre ça typiquement avec :

  • L’éditeur Gutenberg qui rame sur des articles avec 200+ blocs
  • Les imports de médias qui font planter l’admin
  • Le parsing de gros fichiers JSON (pensez aux exports d’Analytics)
  • Les opérations canvas pour la manipulation d’images
  • Les calculs de statistiques en temps réel

L’API FileReader est particulièrement bloquante. Quand vous uploadez un fichier de 50 Mo et que WordPress doit le lire côté client… c’est le drame ! Avec un Web Worker, ce traitement devient invisible pour l’utilisateur.

Architecture des Web Workers : dedicated vs shared

Il existe deux types principaux de Web Workers, et le choix n’est pas anodin :

Dedicated Workers : c’est le modèle classique. Un worker par page, qui vit et meurt avec elle. Parfait pour des tâches ponctuelles comme un import ou un calcul spécifique. Dans WordPress, c’est souvent ce qu’on utilise pour des fonctionnalités admin.

Shared Workers : là, c’est plus sophistiqué. Un seul worker partagé entre plusieurs onglets de votre site WordPress. Utile pour des tâches en continu comme la synchronisation de données ou la mise en cache côté client.

Attention cependant : les Shared Workers ne sont pas supportés par Safari (typique !). Pour une compatibilité maximale dans WordPress, on privilégie généralement les Dedicated Workers.

Le point important à retenir : les Web Workers communiquent uniquement par messages. Pas d’accès au DOM, pas de variables partagées directement. C’est à la fois une limitation et une force pour la stabilité.

Implémentation pratique : Déporter l’import de données

Bon, assez de théorie ! On va maintenant créer un vrai système d’import CSV qui utilise un Web Worker pour traiter les données sans faire planter l’admin WordPress. J’ai développé cette solution pour un client qui devait importer des milliers de produits WooCommerce… et croyez-moi, sans Web Worker, c’était l’enfer !

On va construire ça étape par étape, avec du code que vous pourrez réellement utiliser en production.

Création de la page d’administration WordPress

Commençons par créer notre page d’admin qui va héberger notre import. Dans votre plugin ou thème :

// Ajouter la page au menu admin
add_action('admin_menu', 'csv_import_add_admin_page');
function csv_import_add_admin_page() {
    add_management_page(
        'Import CSV avec Web Worker', // Titre de la page
        'Import CSV',                 // Titre du menu
        'manage_options',            // Capacité requise
        'csv-import-worker',         // Slug de la page
        'csv_import_admin_page'      // Fonction de callback
    );
}

// Enqueue nos scripts uniquement sur notre page
add_action('admin_enqueue_scripts', 'csv_import_enqueue_scripts');
function csv_import_enqueue_scripts($hook) {
    // On charge seulement sur notre page pour éviter les conflits
    if ($hook !== 'tools_page_csv-import-worker') {
        return;
    }
    
    // Script principal de l'import
    wp_enqueue_script(
        'csv-import-main',
        plugin_dir_url(__FILE__) . 'js/csv-import.js',
        array('jquery'),
        '1.0.0',
        true
    );
    
    // Variables JavaScript nécessaires
    wp_localize_script('csv-import-main', 'csvImportAjax', array(
        'ajaxurl' => admin_url('admin-ajax.php'),
        'nonce' => wp_create_nonce('csv_import_nonce'),
        'worker_url' => plugin_dir_url(__FILE__) . 'js/worker.js'
    ));
}

Attention : j’utilise tools_page_ comme préfixe parce que notre page est dans le menu « Outils ». Si vous l’ajoutez ailleurs, le hook changera !

Le fichier worker.js : Le cœur du traitement

Voici notre Web Worker qui va faire tout le travail lourd :

// worker.js - Notre Web Worker pour traiter le CSV

// Écouter les messages du thread principal
self.onmessage = function(e) {
    const { action, data } = e.data;
    
    switch(action) {
        case 'parseCSV':
            parseCSVData(data.csvContent, data.options);
            break;
        case 'validateRow':
            validateSingleRow(data.row, data.rules);
            break;
    }
};

// Fonction principale de parsing CSV
function parseCSVData(csvContent, options = {}) {
    try {
        // Configuration par défaut
        const config = {
            delimiter: options.delimiter || ',',
            skipHeaders: options.skipHeaders || true,
            batchSize: options.batchSize || 100
        };
        
        // Diviser le contenu en lignes
        const lines = csvContent.split('\n').filter(line => line.trim());
        const totalLines = lines.length;
        
        // Récupérer les en-têtes si nécessaire
        let headers = [];
        let startIndex = 0;
        
        if (config.skipHeaders && lines.length > 0) {
            headers = parseCSVLine(lines[0], config.delimiter);
            startIndex = 1;
            
            // Envoyer les headers au thread principal
            self.postMessage({
                type: 'headers',
                headers: headers
            });
        }
        
        // Traitement par batch pour éviter de saturer la mémoire
        for (let i = startIndex; i < lines.length; i += config.batchSize) {
            const batch = [];
            const endIndex = Math.min(i + config.batchSize, lines.length);
            
            // Parser chaque ligne du batch
            for (let j = i; j < endIndex; j++) {
                const rowData = parseCSVLine(lines[j], config.delimiter);
                
                // Créer un objet avec les headers comme clés
                const rowObject = {};
                if (headers.length > 0) {
                    headers.forEach((header, index) => {
                        rowObject[header] = rowData[index] || '';
                    });
                } else {
                    rowObject.data = rowData;
                }
                
                // Valider la ligne
                const validation = validateRowData(rowObject);
                if (validation.isValid) {
                    batch.push({
                        row: j + 1,
                        data: rowObject,
                        status: 'valid'
                    });
                } else {
                    batch.push({
                        row: j + 1,
                        data: rowObject,
                        status: 'invalid',
                        errors: validation.errors
                    });
                }
            }
            
            // Envoyer le batch traité
            self.postMessage({
                type: 'batch',
                batch: batch,
                progress: Math.round((i / totalLines) * 100)
            });
            
            // Petite pause pour ne pas monopoliser le CPU
            // (même si on est dans un worker, c'est une bonne pratique)
            await new Promise(resolve => setTimeout(resolve, 1));
        }
        
        // Signaler la fin du traitement
        self.postMessage({
            type: 'complete',
            totalProcessed: lines.length - startIndex
        });
        
    } catch (error) {
        // Gestion des erreurs
        self.postMessage({
            type: 'error',
            message: error.message,
            stack: error.stack
        });
    }
}

// Parser une ligne CSV (gestion des guillemets et échappements)
function parseCSVLine(line, delimiter = ',') {
    const result = [];
    let current = '';
    let inQuotes = false;
    
    for (let i = 0; i < line.length; i++) {
        const char = line[i];
        
        if (char === '"') {
            if (inQuotes && line[i + 1] === '"') {
                // Guillemet échappé
                current += '"';
                i++; // Skip next quote
            } else {
                // Toggle quote state
                inQuotes = !inQuotes;
            }
        } else if (char === delimiter && !inQuotes) {
            // Nouveau champ
            result.push(current.trim());
            current = '';
        } else {
            current += char;
        }
    }
    
    // Ajouter le dernier champ
    result.push(current.trim());
    return result;
}

// Validation des données (adaptez selon vos besoins)
function validateRowData(rowData) {
    const errors = [];
    
    // Exemple de validation pour un import de produits
    if (!rowData.name || rowData.name.trim() === '') {
        errors.push('Le nom du produit est requis');
    }
    
    if (rowData.price && isNaN(parseFloat(rowData.price))) {
        errors.push('Le prix doit être un nombre');
    }
    
    if (rowData.email && !isValidEmail(rowData.email)) {
        errors.push('Email invalide');
    }
    
    return {
        isValid: errors.length === 0,
        errors: errors
    };
}

// Helper pour valider les emails
function isValidEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
}

Script principal côté WordPress

Maintenant le script qui va orchestrer tout ça dans l’admin :

// csv-import.js - Script principal dans WordPress

jQuery(document).ready(function($) {
    let worker = null;
    let importData = [];
    let isImporting = false;
    
    // Initialiser le worker quand on en a besoin
    function initWorker() {
        if (worker) {
            worker.terminate(); // Nettoyer l'ancien worker
        }
        
        worker = new Worker(csvImportAjax.worker_url);
        
        // Écouter les messages du worker
        worker.onmessage = function(e) {
            const { type, ...data } = e.data;
            
            switch(type) {
                case 'headers':
                    handleHeaders(data.headers);
                    break;
                case 'batch':
                    handleBatch(data.batch, data.progress);
                    break;
                case 'complete':
                    handleComplete(data.totalProcessed);
                    break;
                case 'error':
                    handleError(data.message);
                    break;
            }
        };
        
        worker.onerror = function(error) {
            console.error('Erreur Worker:', error);
            showMessage('Erreur du Web Worker: ' + error.message, 'error');
        };
    }
    
    // Gestionnaire pour le téléchargement de fichier
    $('#csv-file-input').on('change', function(e) {
        const file = e.target.files[0];
        if (!file) return;
        
        // Vérifications basiques
        if (!file.name.toLowerCase().endsWith('.csv')) {
            showMessage('Veuillez sélectionner un fichier CSV', 'error');
            return;
        }
        
        if (file.size > 10 * 1024 * 1024) { // 10MB max
            showMessage('Fichier trop volumineux (max 10MB)', 'error');
            return;
        }
        
        // Lire le fichier
        const reader = new FileReader();
        reader.onload = function(event) {
            const csvContent = event.target.result;
            startImportProcess(csvContent);
        };
        
        reader.onerror = function() {
            showMessage('Erreur lors de la lecture du fichier', 'error');
        };
        
        reader.readAsText(file);
    });
    
    // Démarrer le processus d'import
    function startImportProcess(csvContent) {
        if (isImporting) {
            showMessage('Un import est déjà en cours', 'warning');
            return;
        }
        
        isImporting = true;
        importData = [];
        
        // Initialiser l'interface
        $('#import-progress').show();
        $('#progress-bar').width('0%');
        $('#progress-text').text('Démarrage...');
        $('#import-results').hide();
        
        // Initialiser et démarrer le worker
        initWorker();
        
        const options = {
            delimiter: $('#delimiter-select').val() || ',',
            skipHeaders: $('#skip-headers').is(':checked'),
            batchSize: parseInt($('#batch-size').val()) || 100
        };
        
        worker.postMessage({
            action: 'parseCSV',
            data: { csvContent, options }
        });
    }
    
    // Gérer la réception des headers
    function handleHeaders(headers) {
        console.log('Headers détectés:', headers);
        showMessage('En-têtes détectés: ' + headers.join(', '), 'info');
    }
    
    // Gérer chaque batch de données
    function handleBatch(batch, progress) {
        // Ajouter les données au tableau global
        importData = importData.concat(batch);
        
        // Mettre à jour la barre de progression
        $('#progress-bar').width(progress + '%');
        $('#progress-text').text(`Traitement en cours... ${progress}%`);
        
        // Optionnel: envoyer les données valides au serveur par batch
        const validRows = batch.filter(row => row.status === 'valid');
        if (validRows.length > 0) {
            sendBatchToServer(validRows);
        }
    }
    
    // Import terminé
    function handleComplete(totalProcessed) {
        isImporting = false;
        $('#progress-bar').width('100%');
        $('#progress-text').text('Import terminé !');
        
        // Afficher les résultats
        const validCount = importData.filter(row => row.status === 'valid').length;
        const invalidCount = importData.filter(row => row.status === 'invalid').length;
        
        $('#import-results').show();
        $('#valid-count').text(validCount);
        $('#invalid-count').text(invalidCount);
        $('#total-count').text(totalProcessed);
        
        // Afficher les erreurs s'il y en a
        if (invalidCount > 0) {
            displayErrors();
        }
        
        showMessage(`Import terminé: ${validCount} lignes valides, ${invalidCount} erreurs`, 'success');
    }
    
    // Envoyer un batch au serveur WordPress
    function sendBatchToServer(validRows) {
        $.ajax({
            url: csvImportAjax.ajaxurl,
            type: 'POST',
            data: {
                action: 'process_csv_batch',
                nonce: csvImportAjax.nonce,
                batch_data: JSON.stringify(validRows)
            },
            success: function(response) {
                if (!response.success) {
                    console.error('Erreur serveur:', response.data);
                }
            },
            error: function(xhr, status, error) {
                console.error('Erreur AJAX:', error);
            }
        });
    }
    
    // Afficher les erreurs de validation
    function displayErrors() {
        const errorRows = importData.filter(row => row.status === 'invalid');
        let errorHtml = '<h4>Erreurs détectées:</h4><ul>';
        
        errorRows.slice(0, 10).forEach(row => { // Limiter à 10 erreurs
            errorHtml += `<li>Ligne ${row.row}: ${row.errors.join(', ')}</li>`;
        });
        
        if (errorRows.length > 10) {
            errorHtml += `<li>... et ${errorRows.length - 10} autres erreurs</li>`;
        }
        
        errorHtml += '</ul>';
        $('#error-details').html(errorHtml);
    }
    
    // Gérer les erreurs du worker
    function handleError(message) {
        isImporting = false;
        showMessage('Erreur: ' + message, 'error');
        console.error('Erreur Worker:', message);
    }
    
    // Helper pour afficher les messages
    function showMessage(message, type = 'info') {
        const alertClass = type === 'error' ? 'notice-error' : 
                          type === 'warning' ? 'notice-warning' : 
                          type === 'success' ? 'notice-success' : 'notice-info';
        
        const messageHtml = `<div class="notice ${alertClass} is-dismissible"><p>${message}</p></div>`;
        $('#import-messages').html(messageHtml);
        
        // Auto-dismiss après 5 secondes pour les messages de succès
        if (type === 'success' || type === 'info') {
            setTimeout(() => {
                $('#import-messages').fadeOut();
            }, 5000);
        }
    }
    
    // Nettoyer le worker quand on quitte la page
    $(window).on('beforeunload', function() {
        if (worker) {
            worker.terminate();
        }
    });
});

Interface utilisateur dans WordPress

Voici le HTML de notre page d’admin :

function csv_import_admin_page() {
?>
<div class="wrap">
    <h1>Import CSV avec Web Worker</h1>
    
    <div id="import-messages"></div>
    
    <div class="card">
        <h2>Configuration</h2>
        <table class="form-table">
            <tr>
                <th><label for="csv-file-input">Fichier CSV</label></th>
                <td><input type="file" id="csv-file-input" accept=".csv" /></td>
            </tr>
            <tr>
                <th><label for="delimiter-select">Délimiteur</label></th>
                <td>
                    <select id="delimiter-select">
                        <option value=",">Virgule (,)</option>
                        <option value=";">Point-virgule (;)</option>
                        <option value="\t">Tabulation</option>
                    </select>
                </td>
            </tr>
            <tr>
                <th><label for="skip-headers">Ignorer la première ligne</label></th>
                <td><input type="checkbox" id="skip-headers" checked /></td>
            </tr>
            <tr>
                <th><label for="batch-size">Taille des lots</label></th>
                <td><input type="number" id="batch-size" value="100" min="10" max="1000" /></td>
            </tr>
        </table>
    </div>
    
    <div id="import-progress" style="display:none;" class="card">
        <h2>Progression</h2>
        <div class="progress-wrapper">
            <div id="progress-bar" style="width:0%; height:20px; background:#0073aa;"></div>
        </div>
        <p id="progress-text">En attente...</p>
    </div>
    
    <div id="import-results" style="display:none;" class="card">
        <h2>Résultats</h2>
        <ul>
            <li>Lignes valides: <strong id="valid-count">0</strong></li>
            <li>Lignes avec erreurs: <strong id="invalid-count">0</strong></li>
            <li>Total traité: <strong id="total-count">0</strong></li>
        </ul>
        <div id="error-details"></div>
    </div>
</div>

<style>
.progress-wrapper {
    width: 100%;
    background-color: #f0f0f0;
    border-radius: 4px;
    overflow: hidden;
    margin: 10px 0;
}

#progress-bar {
    height: 20px;
    background: linear-gradient(90deg, #0073aa, #005a87);
    transition: width 0.3s ease;
}

.card {
    background: white;
    padding: 20px;
    margin: 20px 0;
    box-shadow: 0 1px 3px rgba(0,0,0,0.13);
}
</style>
<?php
}

Traitement côté serveur

Et enfin, la partie serveur qui reçoit les données du worker :

// Handler AJAX pour traiter les batches
add_action('wp_ajax_process_csv_batch', 'handle_csv_batch_processing');
function handle_csv_batch_processing() {
    // Vérifications de sécurité
    if (!check_ajax_referer('csv_import_nonce', 'nonce', false)) {
        wp_send_json_error('Nonce invalide');
    }
    
    if (!current_user_can('manage_options')) {
        wp_send_json_error('Permissions insuffisantes');
    }
    
    $batch_data = json_decode(stripslashes($_POST['batch_data']), true);
    
    if (!$batch_data || !is_array($batch_data)) {
        wp_send_json_error('Données invalides');
    }
    
    $processed = 0;
    $errors = [];
    
    foreach ($batch_data as $row) {
        try {
            // Ici, adaptez selon votre cas d'usage
            // Exemple pour créer des posts
            $post_data = array(
                'post_title' => sanitize_text_field($row['data']['name']),
                'post_content' => sanitize_textarea_field($row['data']['description']),
                'post_status' => 'publish',
                'post_type' => 'product' // ou votre custom post type
            );
            
            $post_id = wp_insert_post($post_data);
            
            if (is_wp_error($post_id)) {
                $errors[] = "Ligne {$row['row']}: " . $post_id->get_error_message();
            } else {
                // Ajouter les meta données
                if (isset($row['data']['price'])) {
                    update_post_meta($post_id, '_price', floatval($row['data']['price']));
                }
                $processed++;
            }
            
        } catch (Exception $e) {
            $errors[] = "Ligne {$row['row']}: " . $e->getMessage();
        }
    }
    
    wp_send_json_success(array(
        'processed' => $processed,
        'errors' => $errors
    ));
}

Voilà ! Avec cette implémentation, vous avez un système d’import robuste qui ne bloquera jamais l’interface WordPress, même avec des fichiers de plusieurs milliers de lignes. Le Web Worker traite tout en arrière-plan pendant que l’utilisateur peut continuer à naviguer.

Traitement d’images et manipulation de canvas

L’un des cas d’usage les plus intéressants pour les Web Workers dans WordPress, c’est le traitement d’images avant upload. On peut carrément soulager le serveur en redimensionnant et compressant les images côté client. Et croyez-moi, vos utilisateurs vont adorer ne plus attendre 30 secondes pour uploader une photo de 8Mo !

Redimensionnement et compression côté client

Le principe est simple : on intercepte les fichiers avant qu’ils partent vers le serveur, et on les traite dans un Web Worker. Voici comment créer notre worker de traitement d’image :

// image-processor.worker.js
self.onmessage = function(e) {
    const { file, maxWidth, quality, format } = e.data;
    
    const canvas = new OffscreenCanvas(800, 600);
    const ctx = canvas.getContext('2d');
    
    createImageBitmap(file).then(bitmap => {
        // Calcul des nouvelles dimensions
        const scale = Math.min(maxWidth / bitmap.width, maxWidth / bitmap.height);
        const newWidth = bitmap.width * scale;
        const newHeight = bitmap.height * scale;
        
        canvas.width = newWidth;
        canvas.height = newHeight;
        
        ctx.drawImage(bitmap, 0, 0, newWidth, newHeight);
        
        // Conversion avec compression
        canvas.convertToBlob({
            type: format || 'image/jpeg',
            quality: quality || 0.8
        }).then(blob => {
            self.postMessage({ 
                success: true, 
                blob: blob,
                originalSize: file.size,
                compressedSize: blob.size,
                compressionRatio: ((file.size - blob.size) / file.size * 100).toFixed(1)
            });
        });
    }).catch(error => {
        self.postMessage({ success: false, error: error.message });
    });
};

Ce worker utilise OffscreenCanvas quand c’est disponible (c’est plus performant), sinon on peut faire un fallback avec un canvas classique. La compression JPEG à 0.8 donne généralement un bon compromis qualité/taille.

Intégration avec la médiathèque WordPress

Maintenant, il faut intégrer ça proprement avec wp.media. On va étendre l’uploader natif pour intercepter les fichiers :

// Dans votre fichier admin
const imageWorker = new Worker('/wp-content/themes/votre-theme/js/image-processor.worker.js');

// Extension de l'uploader WordPress
wp.Uploader.queue.on('add', function(file) {
    if (file.type.indexOf('image/') === 0 && file.size > 500000) { // Si > 500Ko
        // Pause de la file d'attente
        this.uploader.stop();
        
        imageWorker.postMessage({
            file: file.getNative(),
            maxWidth: 1920,
            quality: 0.85,
            format: 'image/jpeg'
        });
        
        imageWorker.onmessage = (e) => {
            if (e.data.success) {
                // Remplacement du fichier original
                const newFile = new File([e.data.blob], file.name, {
                    type: e.data.blob.type
                });
                
                // Mise à jour des métadonnées
                file.attachment.set({
                    size: e.data.compressedSize,
                    compression_ratio: e.data.compressionRatio
                });
                
                // Reprise de l'upload
                this.uploader.start();
                
                console.log(`Image compressée : ${e.data.compressionRatio}% d'économie`);
            }
        };
    }
});

Bon, là je simplifie un peu, mais vous voyez l’idée. On intercepte les gros fichiers, on les traite, puis on reprend l’upload normal. L’utilisateur voit juste une petite notification « Optimisation en cours… » pendant quelques secondes.

Optimisation mémoire et gestion des gros fichiers

Attention : traiter des images de 20Mo peut faire planter le navigateur ! Il faut être malin avec la gestion mémoire :

// Gestion par chunks pour les gros fichiers
function processLargeImage(file, chunkSize = 2048) {
    if (file.size > 10000000) { // > 10Mo
        return processImageByChunks(file, chunkSize);
    }
    return processImageNormally(file);
}

function processImageByChunks(file, chunkSize) {
    // Traitement par portions avec canvas temporaires
    const canvas = new OffscreenCanvas(chunkSize, chunkSize);
    const ctx = canvas.getContext('2d');
    
    return createImageBitmap(file).then(bitmap => {
        const chunks = [];
        const cols = Math.ceil(bitmap.width / chunkSize);
        const rows = Math.ceil(bitmap.height / chunkSize);
        
        for (let row = 0; row < rows; row++) {
            for (let col = 0; col < cols; col++) {
                // Traitement chunk par chunk
                const x = col * chunkSize;
                const y = row * chunkSize;
                const width = Math.min(chunkSize, bitmap.width - x);
                const height = Math.min(chunkSize, bitmap.height - y);
                
                canvas.width = width;
                canvas.height = height;
                ctx.drawImage(bitmap, x, y, width, height, 0, 0, width, height);
                
                chunks.push(canvas.convertToBlob());
            }
        }
        
        return Promise.all(chunks);
    });
}

Et n’oubliez pas les fallbacks pour les navigateurs qui ne supportent pas OffscreenCanvas (IE, Safari plus anciens) :

function createWorkerWithFallback() {
    if (typeof OffscreenCanvas !== 'undefined') {
        return new Worker('image-processor.worker.js');
    }
    
    // Fallback : traitement sur le thread principal
    return {
        postMessage: function(data) {
            setTimeout(() => this.processOnMainThread(data), 0);
        },
        processOnMainThread: function(data) {
            // Même logique mais avec canvas classique
            const canvas = document.createElement('canvas');
            // ... traitement synchrone
        }
    };
}

Les métriques que je vois généralement : 60-80% de réduction de taille pour les photos, et un gain de 3-4x sur la vitesse d’upload. Pas mal, non ?