- Il y a 3 ans, Notion a réussi à accélérer son application pour Mac et Windows en mettant en cache les données côté client à l’aide d’une base de données SQLite
- Cette fois, l’entreprise a pu apporter la même amélioration aux utilisateurs qui accèdent à Notion via le navigateur
- Cet article propose une analyse approfondie de la manière dont Notion a amélioré ses performances dans le navigateur à l’aide de
sqlite3 implémenté en WebAssembly (WASM)
- L’utilisation de SQLite a permis d’améliorer de 20 % le temps de navigation entre les pages sur tous les navigateurs modernes
- La différence était particulièrement marquée pour les utilisateurs dont les temps de réponse API étaient élevés à cause de facteurs externes comme la qualité de la connexion Internet
- Par exemple, pour les utilisateurs en Australie, le temps de navigation entre les pages s’est amélioré de 28 %, de 31 % pour les utilisateurs en Chine et de 33 % pour ceux en Inde
Technologies clés : OPFS et Web Workers
- La bibliothèque WASM SQLite utilise une API récente des navigateurs appelée Origin Private File System (OPFS) pour conserver les données d’une session à l’autre
- OPFS n’est utilisable que dans les Web Workers
- On peut considérer un Web Worker comme du code exécuté dans un thread séparé du thread principal, où s’exécute la majeure partie du JavaScript dans le navigateur
- Notion est bundlé avec Webpack, qui fournit une syntaxe simple pour charger un Web Worker
- Un Web Worker a été configuré pour créer un fichier de base de données SQLite via OPFS ou charger un fichier existant, puis y exécuter le code de cache existant
- La bibliothèque Comlink a été utilisée pour gérer plus facilement l’échange de messages entre le thread principal et le Worker
Une approche basée sur SharedWorker
- L’architecture finale repose sur une nouvelle solution proposée par Roy Hashimoto dans une discussion GitHub
- Une approche permettant à un seul onglet d’accéder à SQLite à la fois, tout en autorisant les autres onglets à exécuter eux aussi des requêtes SQLite
- Comment cette nouvelle architecture fonctionne-t-elle ?
- En résumé, chaque onglet dispose de son propre Web Worker dédié capable d’écrire dans SQLite
- Mais un seul onglet peut effectivement utiliser son Web Worker à un instant donné
- Le SharedWorker se charge de déterminer quel est l’« onglet actif »
- Si l’onglet actif est fermé, le SharedWorker sait qu’il doit en choisir un nouveau
- Pour exécuter une requête SQLite, le thread principal de chaque onglet envoie la requête au SharedWorker, qui la redirige vers le Worker dédié de l’onglet actif
- Les onglets peuvent lancer autant de requêtes SQLite simultanées qu’ils le souhaitent, elles sont toujours routées vers l’unique onglet actif
- Chaque Web Worker accède à la base SQLite via l’implémentation OPFS SyncAccessHandle Pool VFS, compatible avec tous les principaux navigateurs
Pourquoi l’approche la plus simple n’a pas fonctionné
- Avant de construire l’architecture décrite ci-dessus, Notion a tenté d’utiliser WASM SQLite avec une méthode plus simple : un Web Worker dédié par onglet, chacun écrivant dans la base SQLite
- Mais aucune des options testées n’était suffisante telle quelle pour répondre aux besoins de Notion
Blocage n°1 : l’isolation cross-origin
- Pour utiliser OPFS via
sqlite3_vfs, le site doit être en état d’« isolation cross-origin »
- Pour ajouter cette isolation à une page, il faut configurer plusieurs en-têtes de sécurité qui limitent les scripts pouvant être chargés
- Mettre en place ces en-têtes peut représenter un travail conséquent
- Notion dépend de nombreux scripts tiers pour faire fonctionner différentes parties de son infrastructure web, si bien qu’atteindre une isolation cross-origin complète aurait obligé à demander à chaque fournisseur de définir de nouveaux en-têtes et de modifier le fonctionnement des iframes — une contrainte difficilement réaliste
- Lors des tests, il a été possible d’obtenir des données de performance importantes en livrant cette variante à un sous-ensemble d’utilisateurs via les Origin Trials de SharedArrayBuffer disponibles sur Chrome et Edge
- L’utilisation de ces Origin Trials a permis de contourner temporairement l’exigence d’isolation cross-origin
Blocage n°2 : les problèmes de corruption
- Lorsqu’OPFS via
sqlite3_vfs a été déployé à un petit nombre d’utilisateurs, de graves bugs ont commencé à apparaître chez certains d’entre eux
- Ces utilisateurs voyaient des données incorrectes sur les pages
- Par exemple, des commentaires attribués au mauvais collègue ou des liens vers de nouvelles pages dont l’aperçu correspondait à une page complètement différente
- En examinant les fichiers de base de données des utilisateurs touchés, un schéma montrait que la base SQLite avait été corrompue d’une manière ou d’une autre
- La sélection de lignes dans certaines tables déclenchait des erreurs et l’inspection directe des lignes révélait des incohérences de données, comme plusieurs lignes avec le même identifiant mais des contenus différents
- Concernant l’origine de cet état de la base SQLite, l’hypothèse était qu’il résultait de problèmes de concurrence
- Plusieurs onglets pouvaient être ouverts, chacun ayant son Web Worker dédié avec une connexion active à la base SQLite
- L’application Notion écrit fréquemment dans le cache à chaque mise à jour reçue du serveur, ce qui faisait que plusieurs onglets écrivaient simultanément dans le même fichier
- Une approche transactionnelle regroupant déjà les requêtes SQLite était en place, mais il était fortement soupçonné que la corruption venait du manque de gestion de la concurrence côté API OPFS
- Notion a donc commencé à journaliser les erreurs de corruption et a tenté plusieurs correctifs de fortune, comme l’ajout de Web Locks et l’autorisation d’écriture dans SQLite uniquement pour l’onglet ayant le focus
- Ces ajustements ont réduit le taux de corruption, mais pas suffisamment pour réactiver la fonctionnalité sur le trafic de production
- Ils ont néanmoins permis de confirmer que les problèmes de concurrence contribuaient fortement à la corruption
- Ce problème n’apparaissait pas dans l’application desktop de Notion
- Sur cette plateforme, un unique processus parent écrit dans SQLite
- L’application peut ouvrir autant d’onglets qu’elle veut, mais un seul thread accède toujours au fichier de base de données
Blocage n°3 : l’alternative ne peut fonctionner que dans un seul onglet à la fois
- La variante OPFS SyncAccessHandle Pool VFS a également été évaluée
- Cette variante ne nécessite pas SharedArrayBuffer, elle peut donc être utilisée dans Safari, Firefox et d’autres navigateurs sans Origin Trial pour SharedArrayBuffer
- Son inconvénient est qu’elle ne peut fonctionner que dans un seul onglet à la fois
- Si un onglet ultérieur essaie d’ouvrir la base SQLite, une erreur est simplement renvoyée
- D’un côté, cela signifie qu’OPFS SyncAccessHandle Pool VFS n’a pas les problèmes de concurrence de la variante OPFS via
sqlite3_vfs
- Cela a été confirmé en l’exposant à un petit nombre d’utilisateurs, sans observer de problème de corruption
- Mais d’un autre côté, cette variante ne pouvait pas être lancée telle quelle, car Notion voulait que tous les onglets des utilisateurs bénéficient de la mise en cache
Résolution du problème
- Le fait qu’aucune des variantes ne soit utilisable telle quelle a conduit à la création de l’architecture SharedWorker décrite plus haut
- Cette architecture est compatible avec l’une comme l’autre de ces variantes de SQLite
- Avec la variante OPFS via
sqlite3_vfs, le fait qu’un seul onglet écrive à la fois permet d’éviter les problèmes de corruption
- Avec la variante OPFS SyncAccessHandle Pool VFS, le SharedWorker permet à tous les onglets de profiter du cache
- Une fois confirmé que cette architecture fonctionnait avec les deux variantes, que le gain de performance était visible dans les métriques et qu’aucun problème de corruption n’apparaissait, il a fallu choisir quelle variante livrer au final
- C’est OPFS SyncAccessHandle Pool VFS qui a été retenu, car il n’impose pas l’exigence d’isolation cross-origin, ce qui n’empêche pas un déploiement sur des navigateurs autres que Chrome et Edge
Atténuer les régressions de performance
- Lorsque cette amélioration a commencé à être déployée auprès des utilisateurs, quelques régressions de performance ont été observées, notamment un ralentissement des temps de chargement
Le chargement des pages est devenu plus lent
- Le premier constat a été que la navigation entre les pages Notion devenait plus rapide, mais que le chargement initial de page devenait plus lent
- Le profilage a montré que le chargement de page n’est généralement pas limité par la récupération des données
- Le code de démarrage de l’application Notion exécute d’autres tâches (analyse du JS, initialisation de l’application, etc.) pendant qu’il attend la fin des appels API, si bien qu’il bénéficie moins du cache SQLite que la navigation
- Le ralentissement venait du fait que l’utilisateur devait télécharger et traiter la bibliothèque WASM SQLite
- Cela bloquait le processus de chargement de la page, empêchant d’autres tâches de chargement de se produire en parallèle
- Comme cette bibliothèque pèse plusieurs centaines de kilo-octets, le temps supplémentaire était nettement visible dans les métriques
- Pour corriger cela, la manière de charger la bibliothèque a été légèrement modifiée
- WASM SQLite a été chargé de manière entièrement asynchrone, sans bloquer le chargement de la page
- Cela signifiait que les données de la page initiale seraient rarement chargées depuis SQLite
- Mais ce compromis était acceptable, car il a été déterminé objectivement que le gain de vitesse obtenu en chargeant la page initiale depuis SQLite ne compensait pas le ralentissement dû au téléchargement de la bibliothèque
- Après ce changement, les métriques de chargement initial de page sont devenues identiques entre le groupe test et le groupe témoin de l’expérience
Les appareils lents ne profitaient pas du cache
- Un autre phénomène observé dans les métriques était que, si le temps médian de navigation d’une page Notion à une autre baissait, le temps au 95e percentile augmentait
- Certains appareils, comme des téléphones mobiles ouvrant Notion dans un navigateur, ne profitaient pas du cache et voyaient même leurs performances se dégrader
- La réponse à cette énigme est venue d’une enquête antérieure menée par l’équipe mobile
- Lors de l’implémentation de ce cache dans l’application mobile native, certains appareils, comme d’anciens téléphones Android, lisaient très lentement depuis le disque
- On ne pouvait donc pas supposer que charger les données depuis le cache disque serait forcément plus rapide que les charger depuis l’API
- L’enquête mobile a montré qu’une logique faisait déjà « courir » en parallèle deux requêtes asynchrones (SQLite et l’API) lors du chargement de page
- Ce mécanisme a simplement été réimplémenté dans le chemin de code des clics de navigation
- Cela a permis d’aligner le 95e percentile du temps de navigation entre les deux groupes de l’expérience
Conclusion
- Apporter dans le navigateur les gains de performance de SQLite pour Notion n’a pas été sans difficulté
- L’équipe s’est notamment heurtée à une série de problèmes encore mal connus liés à des technologies récentes, et en a tiré plusieurs enseignements :
- OPFS ne gère pas élégamment la concurrence par défaut. Les développeurs doivent en être conscients et concevoir leur architecture en conséquence
- Les Web Workers et les SharedWorkers (ainsi que leur cousin Service Workers, non abordé ici) ont des rôles différents, et il peut être utile de les combiner selon les besoins
- Au printemps 2024, implémenter complètement l’isolation cross-origin dans une application web sophistiquée n’est pas simple, surtout lorsqu’elle utilise des scripts tiers
- En mettant en cache les données des utilisateurs dans le navigateur avec SQLite, Notion a constaté l’amélioration de 20 % du temps de navigation mentionnée plus haut, sans observer de dégradation des autres métriques
- Point important : aucun problème lié à une corruption SQLite n’a été observé
- Selon l’équipe, le succès et la stabilité de cette approche finale doivent beaucoup à l’équipe en charge de l’implémentation WASM officielle de SQLite, ainsi qu’à Roy Hashimoto, qui a partagé une approche expérimentale avec le public
6 commentaires
C’est pour ça que les services qui doivent collaborer avec des tiers devraient activer l’isolation cross-origin dès leur tout premier lancement...
Oh, ravi de te voir, cometkim !
Chez moi, quand j’ouvre une page Notion dans Firefox, elle se fige et devient inutilisable ; est-ce que ça pourrait être à cause de ça… (l’application Notion fonctionne bien, donc j’utilise celle-là pour le moment)
Probablement. Enda aussi ne prend en charge l’écriture de fichiers locaux que sur Chrome et Edge.
J’ai déjà eu ce problème sur un vieux portable Linux ; en mode privé, ça fonctionnait.