Contents
Introducción
Los Web Workers permiten ejecutar scripts en hilos en segundo plano fuera del hilo principal de la UI. En el admin de WordPress esto es muy útil para procesar grandes volúmenes de datos (parsing de CSV, cálculo intensivo, procesamiento de imágenes, generación de índices, validaciones masivas) sin bloquear la interfaz de administración. Este tutorial muestra cómo integrarlos correctamente en el área admin de WordPress: cómo registrar y cargar los ficheros, comunicar la UI con el worker, manejar tránsito de datos, usar transferable objects, implementar un pool de workers, y qué consideraciones de seguridad y rendimiento tener en cuenta.
Requisitos y compatibilidad
- Navegadores: La mayoría de navegadores modernos soportan Web Workers (Chrome, Firefox, Edge, Safari). En el admin de WP normalmente se usa un navegador moderno aun así, se debe prever un fallback si es imprescindible cubrir entornos antiguos.
- Entorno WP: Archivos de worker accesibles vía URL (plugin o tema). No se puede acceder al DOM desde el worker.
Arquitectura recomendada
Separar el código en tres piezas principales:
- Fichero del worker (p. ej. worker-data-processor.js), servido como archivo estático desde el plugin/tema.
- Script admin que corre en el hilo principal (p. ej. admin.js) y crea/comunica con el worker.
- Enqueue y localización de datos desde PHP para pasar URL del worker, admin-ajax.php o nonces.
Puntos clave de diseño
- Enviar solo los datos necesarios al worker y recibir resultados ya procesados.
- Usar transferable objects (ArrayBuffer) para mover grandes bloques de memoria sin copia.
- No exponer secretos en código cliente pasar nonces y validar en servidor si el worker hace requests al servidor.
- Terminar workers cuando no sean necesarios para liberar recursos.
Ejemplo completo paso a paso
1) Fichero del worker (worker-data-processor.js)
Ejemplo: worker que parsea CSV en chunks, publica progreso y puede enviar los resultados de vuelta.
self.onmessage = function(e) { const msg = e.data if (!msg !msg.action) return try { if (msg.action === parseCSV) { const text = msg.text const chunkSize = 100000 // caracteres por chunk arbitrario const total = text.length let offset = 0 let rows = [] while (offset < total) { const chunk = text.slice(offset, offset chunkSize) // Parsing simple por líneas en producción sustituir por parser robusto const lines = chunk.split(/r?n/) for (let i = 0 i < lines.length i ) { const line = lines[i].trim() if (line) { // ejemplo: coma separador. Mejor usar análisis robusto si hay comillas const cols = line.split(,) rows.push(cols) } } offset = chunkSize // emitir progreso const progress = Math.min(100, Math.round((offset / total) 100)) self.postMessage({ progress: progress }) } // Resultado final self.postMessage({ result: rows }) // Opcional: cerrar worker // self.close() } if (msg.action === fetchSave) { // Un worker puede usar fetch para comunicarse con el servidor. // msg.ajaxUrl y msg.nonce deben provenir del hilo principal via postMessage. fetch(msg.ajaxUrl, { method: POST, headers: { Content-Type: application/x-www-form-urlencodedcharset=UTF-8 }, body: action=my_worker_savenonce= encodeURIComponent(msg.nonce) data= encodeURIComponent(JSON.stringify(msg.payload)) }) .then(response =gt response.json()) .then(json =gt { self.postMessage({ saved: true, serverResponse: json }) }) .catch(err =gt { self.postMessage({ error: err.message String(err) }) }) } } catch (err) { self.postMessage({ error: err.message String(err) }) } }
2) PHP: Enqueue del script admin y pasar URL del worker y nonces
Registrar y encolar el script admin usar wp_localize_script para pasar datos al script cliente (workerUrl, ajaxUrl, nonce).
add_action(admin_enqueue_scripts, my_plugin_admin_enqueue) function my_plugin_admin_enqueue(hook) { // limitar a la página admin donde se necesita si procede: // if (hook !== toplevel_page_my_plugin) return wp_enqueue_script( my-plugin-admin, plugins_url(admin.js, __FILE__), array(), 1.0, true ) worker_url = plugins_url(worker-data-processor.js, __FILE__) wp_localize_script(my-plugin-admin, MyWorkerData, array( workerUrl =gt worker_url, ajaxUrl =gt admin_url(admin-ajax.php), nonce =gt wp_create_nonce(my_worker_nonce), )) } // Handler del AJAX si el worker solicita guardar algo en servidor: add_action(wp_ajax_my_worker_save, my_worker_save) function my_worker_save() { check_ajax_referer(my_worker_nonce, nonce) raw = isset(_POST[data]) ? wp_unslash(_POST[data]) : data = json_decode(raw, true) if (data === null) { wp_send_json_error(array(msg =gt JSON inválido)) } // Validar y sanitizar data según el caso // Ejemplo: guardar en opción (evitar datos excesivamente grandes) update_option(my_worker_last_result, data) wp_send_json_success(array(saved =gt true)) }
3) Script admin (admin.js): crear y comunicar con el worker
// MyWorkerData fue creado por wp_localize_script (function(){ // Creación del worker con fallback a Blob si workerUrl no disponible o el navegador no soporta Worker function createWorker(url) { if (window.Worker) { try { return new Worker(url) } catch (err) { // Si URL no sirve (CORS, path incorrecto) intentar crear via Blob (si se dispone del código) console.warn(No se pudo crear worker desde URL, intentando fallback:, err) } } return null } // Crear worker principal var worker = createWorker(MyWorkerData.workerUrl) // Si no se pudo crear desde URL, se puede implementar un fallback con Blob si se incluye el código del worker aquí. if (!worker window.Worker) { // Ejemplo sencillo: crear worker inline con función mínima var workerBlob = new Blob([ self.onmessage = function(e) { self.postMessage({ result: fallback worker processed: JSON.stringify(e.data) }) } ], { type: text/javascript }) worker = new Worker(URL.createObjectURL(workerBlob)) } // Fallback total: si no hay Worker, usar setTimeout para procesar en hilo principal sin bloquear de forma prolongada var supportsWorker = !!worker function fallbackProcess(obj, cbProgress, cbDone) { // procesamiento simple chunked con setTimeout para permitir rendering var text = obj.text var i = 0 var lines = text.split(/r?n/) var batch = 500 var result = [] function processChunk() { var end = Math.min(i batch, lines.length) for ( i lt end i ) { if (lines[i].trim()) result.push(lines[i].split(,)) } if (cbProgress) cbProgress(Math.round((i / lines.length) 100)) if (i lt lines.length) { setTimeout(processChunk, 0) } else { if (cbDone) cbDone(result) } } processChunk() } // API expuesto para el admin: llamar a processCSV(text, onProgress, onComplete) window.MyAdminWorker = { processCSV: function(text, onProgress, onComplete) { if (supportsWorker) { worker.onmessage = function(ev) { if (ev.data.progress !== undefined) { if (onProgress) onProgress(ev.data.progress) } else if (ev.data.result !== undefined) { if (onComplete) onComplete(null, ev.data.result) } else if (ev.data.error) { if (onComplete) onComplete(new Error(ev.data.error)) } else { if (onComplete) onComplete(null, ev.data) } } worker.onerror = function(err) { if (onComplete) onComplete(err) } worker.postMessage({ action: parseCSV, text: text }) } else { // fallback sin worker fallbackProcess({ text: text }, onProgress, function(result) { onComplete(null, result) }) } }, saveResultToServer: function(payload, onDone) { if (supportsWorker) { worker.onmessage = function(ev) { if (ev.data.saved) { onDone(null, ev.data.serverResponse) } else if (ev.data.error) { onDone(new Error(ev.data.error)) } } worker.postMessage({ action: fetchSave, payload: payload, ajaxUrl: MyWorkerData.ajaxUrl, nonce: MyWorkerData.nonce }) } else { // fallback en UI thread var xhr = new XMLHttpRequest() xhr.open(POST, MyWorkerData.ajaxUrl, true) xhr.setRequestHeader(Content-Type, application/x-www-form-urlencoded charset=UTF-8) xhr.onload = function() { try { var json = JSON.parse(xhr.responseText) onDone(null, json) } catch (e) { onDone(e) } } xhr.onerror = function(e) { onDone(e) } xhr.send(action=my_worker_savenonce= encodeURIComponent(MyWorkerData.nonce) data= encodeURIComponent(JSON.stringify(payload))) } }, terminate: function() { if (worker) { worker.terminate() worker = null } } } })()
Transferable objects y performance
Cuando se transfieren grandes bloques de datos (ArrayBuffer) es más eficiente transferir la propiedad del buffer en lugar de clonarlo. Ejemplo de envío de buffer desde el hilo principal:
// Crear buffer (ejemplo 8MB) var buffer = new ArrayBuffer(8 1024 1024) if (worker) { worker.postMessage({ type: processBuffer, buffer: buffer }, [buffer]) // Tras esto, buffer queda neutro (neutered) y ya no contiene datos en el hilo principal }
En el worker se recibe el buffer y se procesa sin copies adicionales.
Pool de workers para paralelizar
Si tienes muchas tareas pequeñas o loteables, crear un pool de N workers y repartirlas reduce latencia. Ejemplo esquemático:
function WorkerPool(size, workerUrl) { this.workers = [] this.queue = [] this.next = 0 for (var i = 0 i lt size i ) { this.workers.push(new Worker(workerUrl)) } this.runTask = function(taskData, callback) { var worker = this.workers[this.next] this.next = (this.next 1) % this.workers.length var onMessage = function(ev) { callback(null, ev.data) worker.removeEventListener(message, onMessage) } var onError = function(err) { callback(err) worker.removeEventListener(error, onError) } worker.addEventListener(message, onMessage) worker.addEventListener(error, onError) worker.postMessage(taskData) } this.terminateAll = function() { this.workers.forEach(function(w){ w.terminate() }) this.workers = [] } }
Comunicación worker → servidor
Un worker puede llamar a fetch/XHR directamente. No tiene acceso a cookies de forma distinta al hilo principal, pero sí puede enviar datos a admin-ajax.php siempre y cuando se le pase la URL y un nonce validado. Nunca expongas claves privadas en los datos pasados al worker.
Mejores prácticas y consideraciones
- Validación y seguridad: Validar nonces en el servidor (check_ajax_referer). Sanitizar cualquier dato que se almacene.
- No manipular el DOM desde el worker: Los workers no tienen acceso al DOM. Toda actualización de UI debe hacerse en el hilo principal tras recibir postMessage.
- Evitar pasar objetos gigantes innecesarios: Transferir solo lo imprescindible y usar ArrayBuffers cuando corresponda.
- Manejo de errores: Implementar onerror/onmessage para capturar excepciones y mostrar estados al usuario.
- Recursos: Terminar workers con worker.terminate() cuando no sean necesarios.
- Pool y hardwareConcurrency: usar navigator.hardwareConcurrency para elegir tamaño del pool si se paraleliza intensamente.
- Pruebas: Probar con datasets grandes y medir memoria y tiempo comprobar comportamiento en distintos navegadores.
Fallbacks y degradación elegante
Si el navegador no soporta Web Workers, implementar una versión basada en chunking con setTimeout para evitar bloqueos largos. Si el procesamiento es crítico y el entorno no soporta Web Workers, considerar mover la carga al servidor (job queue) y utilizar AJAX para procesar en background en el servidor.
Recursos útiles
Resumen práctico
Para integrar Web Workers en el admin de WordPress:
- Coloca el fichero worker como archivo público en tu plugin/tema.
- Encola tu script admin y pasa la URL del worker y nonces con wp_localize_script.
- Comunica la UI con el worker vía postMessage/onmessage maneja progreso y errores.
- Usa transferable objects para grandes buffers y considera un pool para tareas paralelas.
- Valida y sanitiza siempre en servidor termina workers cuando terminen su trabajo.
Implementación responsable
Implementar Web Workers en el admin mejora la experiencia al evitar congelaciones, pero requiere diseñar correctamente la comunicación, seguridad y gestión de recursos. Con las prácticas descritas podrás procesar datos pesados en segundo plano sin comprometer la usabilidad del panel de administración.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |