Web workers
Lionel Médini
M1IF13 - Programmation Web avancée et mobile
Université Claude Bernard Lyon 1
Faire faire plusieurs “trucs” à un navigateur “en même temps”
Fonctionnement de JavaScript
Moteur JS interne à un navigateur
→ Modèle d'exécution assez simple
Principes de base d’un moteur JS (non optimisé)
Principes de base d’un moteur JS (non optimisé)
while (queue.waitForMessage()) {
queue.processNextMessage();
}
Traitement d'un message
Ajout de message
Exemple 1 :
function timeoutLog() {
console.log("Fin timeout");
}
function exemple1() {
setTimeout(timeoutLog, 0);
console.log("Fin test");
}
Remarque : c'est la raison pour laquelle le deuxième argument de setTimeout (et setInterval) est une durée minimale et pas garantie…
Exemple 2 :
monBouton.addEventListener('click', function () { console.log('Cliqué'); });
function exemple2() {
let res = 0;
for(let i=0; i < 1000000000; i++) {
//calcul quelconque qui prend du temps mais n'ajoute rien à la pile
res += 1 / (i * i);
}
console.log("Fini de calculer");
}
Plusieurs approches
setTimeout
, setInterval
async
, defer
Dans tous les cas
Les Web workers
LocalStorage
, SessionStorage
)messages
"For example, it would be inappropriate to launch one worker for each pixel of a four megapixel image."
message port
message ports
MessageChannel
est implicitement créé lors de l'instanciation / la connexion à un workerMessagePort
permet de communiquer de chaque côtéMessage
Exemple :
worker.onmessage = function (event) { ... };
worker.postMessage({localObject //to be cloned}, [buffer //to be transfered]);
Voir cours Web messaging pour plus de détails
<body>
<p>The highest prime number discovered so far is:
<output id="result"></output>
</p>
<script>
var worker = new Worker('worker.js');
worker.onmessage = function (event) {
document.getElementById('result').textContent = event.data;
};
</script>
</body>
let n = 1;
search: while (true) {
n += 1;
for (var i = 2; i <= Math.sqrt(n); i += 1)
if (n % i == 0)
continue search;
// found a prime!
postMessage(n);
}
L'interface WebIDL Worker (dedicated worker)
EventTarget
(interface DOM de gestion d'événements)AbstractWorker
(commune aux deux types de workers mais ne contient qu'une méthode onerror)Worker(DOMString scriptURL [, WorkerOptions options])
: crée un dedicated worker si l'URL correspond à un script valideonmessage
: permet d'associer un handler d'événements “simple” au workerterminate()
: supprime les tâches de la file, arrête le script et vide la file de messagespostMessage()
: envoie des données (chaînes de caractères, objets…) au workervar worker = new Worker('task.js');
ou
var worker = new Worker('task.js', { type: "module" });
worker.terminate()
self.close()
(cf. interface DedicatedWorkerGlobalScope
)Avec un worker depuis le script “père”
avec onmessage
worker.onmessage = function (event) {
... //Récupération des données avec event.data
};
avec addEventListener()
worker.addEventListener('message', function(event) { ... }, false);
worker.postMessage('Hello World');
Avec le script “père” depuis un worker
avec onmessage
onmessage = function (event) {
... //Récupération des données avec event.data
};
avec addEventListener()
addEventListener('message', function(event) { ... }, false);
postMessage('Hello World');
Avec le script “père” depuis un worker
onmessage
et postMessage()
sous-entend la référence à l'objet this
(propriétés de l'objet Worker
)self : self.onmessage
et self.postMessage()
iframe
<pre id="log">Log:</pre>
<script>
const worker = new SharedWorker('test.js');
const log = document.getElementById('log');
worker.port.addEventListener('message', function(e) {
log.textContent += '\n' + e.data;
}, false);
worker.port.start();
worker.port.postMessage('ping');
</script>
<iframe src="inner.html"></iframe>
iframe
se connecte également au worker<pre id=log>Inner log:</pre>
<script>
const worker = new SharedWorker('test.js');
const log = document.getElementById('log');
worker.port.onmessage = function(e) {
log.textContent += '\n' + e.data;
};
</script>
let count = 0;
onconnect = function(e) {
count += 1;
var port = e.ports[0];
port.postMessage('Hello World! You are connection #' + count);
port.onmessage = function(e) {
port.postMessage('pong');
}
}
L'interface WebIDL SharedWorker
EventTarget
(interface DOM de gestion d'événements)AbstractWorker
(commune aux deux types de workers mais ne contient qu'une méthode onerror)SharedWorker(DOMString scriptURL, optional DOMString name)
: crée un shared worker correspondant au nom name
si l'URL correspond à un script valideport
: renvoie le MessagePort
associé au worker ; toutes les opérations de gestion (start()
et close()
) et de communication (onmessage()
et postMessage()
) sont faites à partir de cet objetCréation d'un shared worker
var worker = new Worker('task.js', 'worker1');
Création d'un shared worker
worker.port.start();
Fermeture des ports
close()
du MessagePort
worker.port.close();
var port = e.ports[0];
port.close();
worker.port.onmessage;
...
worker.port.postMessage()
Même fonctionnement que pour les dedicated workers
MessagePort
à travers l'événement connect
:
Premier (et seul) élément du tableau ports
let port;
onconnect = function(e) {
port = e.ports[0];
...
}
...
port.onmessage = function(e) {
port.postMessage(...); // not e.ports[0].postMessage!
// e.target.postMessage('pong'); would work also
}
The global scope is the "inside" of a worker.
L'interface WebIDL WorkerGlobalScope
EventTarget
WorkerGlobalScope self
: renvoie le scope du worker (permet d'y associer des handlers d'événements)WorkerLocation location
: renvoie l'URL absolue du script utilisé pour initialiser le worker (origine)close()
: arrête les tâches courantes et met l'objet WorkerGlobalScope
à null
onerror
, onlanguagechange
, onoffline
, ononline
DedicatedWorkerGlobalScope
et SharedWorkerGlobalScope
L'interface WebIDL DedicatedWorkerGlobalScope
WorkerGlobalScope
postMessage()
: …
L'interface WebIDL SharedWorkerGlobalScope
WorkerGlobalScope
name
: nom donné à la création du workerapplicationCache
: gestion de l'application offline par le navigateuronconnect
: callback de l'événement connect
;
L'interface WebIDL WorkerUtils
WorkerNavigator navigator
: permet d'obtenir des informations sur le navigateur (user-agent id, online)importScripts(DOMString... urls)
: permet de télécharger des bibliothèques de code (avec restriction d'origine) importScripts('script1.js', 'script2.js');
Remarque : on peut aussi utiliser la directive JS import
si on a déclaré un worker de type “module”
Synchronisation de données avec un shared worker
EventTarget
)Source Digital Ocean
Initialisation d'un worker (côté “client”)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js', { scope: '/base_url/' })
.then(function(registration) {
console.log('ServiceWorker registration successful with scope', registration.scope);
})
.catch(function(error){
console.log('ServiceWorker registration failed ', error);
});
}
Remarques :
navigator.serviceWorker
(de type ServiceWorkerContainer) avant de l'utiliseronStateChange
qui renvoie le ServiceWorkerState (état de fonctionnement : “installing”, “activated”…) du workerInitialisation du cache (côté worker)
const CACHE_NAME = 'v1';
const CACHE_URLs = [ '/base_url/', '/base_url/index.html', '/base_url/css/style.css', '/base_url/js/app.js','https://code.jquery.com/jquery-3.4.1.min.js', ... ];
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(CACHE_URLs);
})
);
});
Remarques :
cache
est de type CacheStorage
; l'interface CacheStorage permet d'accéder à un tableau d'objets CacheInterception d'un FetchEvent et renvoi des ressources en cache (côté worker)
self.addEventListener('fetch', function(event) {
console.log('Intercepted fetch request: ', event.request.url);
event.respondWith(
caches.match(event.request).then(function(response) {
if (response) {
console.log('Response found in cache.');
return response;
}
console.log('Response NOT found in cache. Fetching...');
return fetch(event.request);
})
);
});
Remarques :
caches.match()
cherche dans tous les caches si la réponse est disponibleevent.respondWith()
intercepte un fetch()
et renvoie une promesseInterception d'un FetchEvent, renvoi des ressources en cache et caching des autres (côté worker)
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then((r) => {
if (response) { return response; }
console.log('Response NOT found in cache. Fetching...');
return fetch(e.request).then((response) => {
return caches.open(cacheName).then((cache) => {
console.log('...and caching new resource: '+e.request.url);
cache.put(e.request, response.clone());
return response;
});
});
})
);
});
Remarques :
L'interface WebIDL ServiceWorker
fetch
message
, messageerror
self.serviceWorker.postMessage()
Compromis entre fraîcheur / performance / offline
return response || fetch(event.request)
fetch(event.request).catch(function() {
return caches.match(event.request);
})
Mise à jour du cache asynchrone
Mise à jour du contenu de la page
Plus de détails ici
Utilisation d'un Service Worker avec IndexedDB (source)
Ajout de données
self.addEventListener('activate', function(event) {
event.waitUntil(
idb.open('products', 1, function(upgradeDB) {
var store = upgradeDB.createObjectStore('beverages', {keyPath:'id'});
store.put({id: 123, name: 'coke', price: 10.99, quantity: 200});
});
);
});
Récupération de données
function readDB() {
idb.open('products', 1).then(function(db) {
var tx = db.transaction(['beverages'], 'readonly');
var store = tx.objectStore('beverages');
return store.getAll();
}).then(function(items) { ... });
}
Fallbacks
caches.match(event.request).then(function(response) {
// récupérer la réponse depuis Fetch / Cache
if (response.status === 404) {
return caches.match('pages/404.html');
}
return response;
}).catch(function() {
return caches.match('/offline.html');