Guide développeurs

Intégrer MapLibre GL JS dans une application Angular, du paquet npm au rendu serveur

Pour intégrer MapLibre GL JS dans Angular, il faut quatre gestes : créer la carte après le rendu et hors de la zone, attendre le style avant d'ajouter sources et couches, laisser un signal piloter setData, et appeler map.remove() à la destruction. Depuis MapLibre 6, publiée le 22 juillet 2026, il en faut un cinquième : servir soi-même le fichier du worker. Le composant de cette page a été vérifié le 27 septembre 2026 contre maplibre-gl 6.11.2 et @angular/core 22.2.0.

Par Lucas Tesnier, co-fondateur de La Trace · Publié le 27 septembre 2026 · Lecture 10 minutes

  • 6.11.2la version de maplibre-gl sur npm, avec @angular/core en 22.2.0Registre npm, consulté le 27/09/2026
  • 22 juillet 2026MapLibre 6.0 passe en ESM seul : l'import par défaut ne compile plusCHANGELOG de MapLibre GL JS
  • 296 Kocompressés à charger pour la carte, hors feuille de style et workerMesure La Trace, gzip -9 sur maplibre-gl 6.11.2, 27/09/2026
  • 720 806sessions sur les cartes La Trace du 10 avril au 26 septembre 2026, 353 s en moyenneMesure La Trace en base, 27/09/2026

L'essentiel

  • MapLibre 6 est livrée en ESM seul. import maplibregl from 'maplibre-gl' échoue, il faut des imports nommés.
  • Avec le builder esbuild d'Angular, le worker ne se trouve pas seul : copiez maplibre-gl-worker.mjs dans les assets et appelez setWorkerUrl().
  • Créez la carte dans afterNextRender, hors de la zone. Ce rappel ne tourne jamais côté serveur.
  • Un effect() qui appelle setData remplace les abonnements RxJS. Un seul endroit écrit dans la source.
  • MapLibre suit la taille de son conteneur depuis la version 3. Le ResizeObserver maison est de trop.

Ce qui a changé en 2026 pour une carte dans Angular

Le 22 juillet 2026, MapLibre GL JS publie sa version 6.0. Le lecteur qui met à jour un projet existant voit sa compilation tomber sur la première ligne : le paquet n'a plus d'export par défaut. Nous l'avons vérifié le 27 septembre avec TypeScript : import maplibregl from 'maplibre-gl' rend l'erreur TS1192, « has no default export ». La même version abandonne WebGL 1, retire le second paramètre de GeoJSONSource.setData et ne publie plus le fichier UMD qu'on chargeait par une balise <script>.

Côté Angular, le changement date du 19 novembre 2025 : la version 21 rend l'application sans zone par défaut. NgZone.runOutsideAngular reste compatible, la documentation le dit en toutes lettres, mais il ne protège plus rien dans une application neuve. Il reste indispensable dans toutes celles qui tournent encore sous zone.js.

MapLibre elle-même est le fork libre de Mapbox GL JS, né quand Mapbox a changé de licence en décembre 2020. Elle est sous licence BSD à trois clauses. Nos cartes tournent sur un moteur construit au-dessus d'elle, dans une application Angular : les motifs qui suivent sont ceux qui tiennent sous des centaines de milliers de sessions.

La recette en cinq gestes

Le guide de migration de MapLibre 6 le dit sans détour : avec un outil de build, import.meta.url ne retrouve pas le fichier du worker, et chaque projet doit appeler setWorkerUrl() une fois. Angular construit avec esbuild par défaut depuis sa version 17, il est donc concerné. Voici l'ordre qui tient avec les versions de septembre 2026.

  1. 1

    Installer le paquet, sa feuille de style et son worker

    npm install maplibre-gl, puis la feuille de style dans styles et le fichier du worker dans assets du fichier angular.json. Sans le worker, la carte reste grise.

  2. 2

    Créer la carte après le premier rendu, hors de la zone

    afterNextRender garantit que le conteneur existe dans le DOM et ne s'exécute jamais sur le serveur. runOutsideAngular sort la boucle d'animation de MapLibre de la détection de changement.

  3. 3

    Attendre le style avant toute source ou couche

    addSource avant la fin du chargement du style lève une erreur. On écoute style.load, qui se déclenche au premier style et à chaque setStyle.

  4. 4

    Laisser un signal piloter les données

    Un effect() lit le signal des données et appelle setData sur la source existante. La carte ne recrée jamais ses couches pour un simple changement de données.

  5. 5

    Détruire proprement

    DestroyRef.onDestroy appelle map.remove(), qui libère le DOM, les écouteurs, les workers et le contexte WebGL.

La configuration de build tient en deux entrées : la feuille de style dans styles, le worker copié tel quel à la racine du site.

angular.json, dans build.options

"styles": ["node_modules/maplibre-gl/dist/maplibre-gl.css", "src/styles.css"],
"assets": [
  { "glob": "**/*", "input": "public" },
  { "glob": "maplibre-gl-worker.mjs", "input": "node_modules/maplibre-gl/dist", "output": "/" }
]

Créer la carte après le rendu, hors de la zone Angular

Le piège le plus coûteux ne se voit pas au premier essai. Sous zone.js, chaque écouteur que MapLibre pose sur le canvas, glissé, molette, toucher, et chaque image de sa boucle requestAnimationFrame, déclenche une détection de changement sur toute l'application. Sur une page légère, rien ne se sent. Sur une page avec un panneau de résultats et cent marqueurs, le glissé saccade. On le lit au profileur : des tâches longues à chaque déplacement, sans aucun changement d'état.

La règle : la carte naît hors de la zone, et seuls les gestes qui changent l'état de l'application y reviennent. Le module est importé à la demande, ce qui sort ses 296 Ko compressés du paquet initial.

carte.component.ts, la création

import { Component, DestroyRef, ElementRef, NgZone, afterNextRender, effect, inject, input, output, signal, viewChild } from '@angular/core';
import type { GeoJSONSource, Map as MapLibreMap, Marker } from 'maplibre-gl';
import type { FeatureCollection } from 'geojson';

@Component({
  selector: 'app-carte',
  template: `<div #conteneur class="carte"></div>`,
  styles: `.carte { height: 480px; }`,
})
export class CarteComponent {
  readonly styleUrl = input.required<string>();
  readonly traces = input<FeatureCollection>({ type: 'FeatureCollection', features: [] });

  private readonly conteneur = viewChild.required<ElementRef<HTMLDivElement>>('conteneur');
  private readonly zone = inject(NgZone);
  private readonly carte = signal<MapLibreMap | null>(null);
  private maplibre?: typeof import('maplibre-gl');
  private detruit = false;

  constructor() {
    afterNextRender(async () => {
      const maplibre = await import('maplibre-gl');
      if (this.detruit) return;
      this.maplibre = maplibre;
      maplibre.setWorkerUrl(new URL('maplibre-gl-worker.mjs', document.baseURI).href);

      const map = this.zone.runOutsideAngular(() => new maplibre.Map({
        container: this.conteneur().nativeElement,
        style: this.styleUrl(),
        center: [5.8, 45.35],
        zoom: 10,
      }));
      map.on('style.load', () => this.ajouterCouches(map));
    });
  }
}

La garde detruit n'est pas décorative. Entre le rendu et l'arrivée du module, l'utilisateur peut avoir quitté la page : sans elle, une carte naît dans un composant déjà détruit et personne ne la libère.

À emporter : la checklist de mise en production d'une carte web

Recevoir la checklist

Couches et données : attendre le style, puis laisser un signal piloter setData

Avant la version 6, setData(data, true) attendait la fin du traitement dans le worker et renvoyait la source. Le 22 juillet 2026, ce second paramètre a disparu : le même appel ne compile plus, TypeScript répond « Expected 1 arguments, but got 2 ». La méthode rend désormais une promesse.

La règle : une source se crée une fois, à style.load, puis ne reçoit plus que setData. Un effect() sur le signal des données fait le lien. Il se relance quand les données changent, et aussi quand la carte devient prête, ce qui règle l'ordre d'arrivée sans une ligne de RxJS.

carte.component.ts, la source et le signal

private ajouterCouches(map: MapLibreMap) {
  if (!map.getSource('traces')) {
    map.addSource('traces', { type: 'geojson', data: this.traces() });
    map.addLayer({
      id: 'traces-ligne',
      type: 'line',
      source: 'traces',
      layout: { 'line-join': 'round', 'line-cap': 'round' },
      paint: { 'line-color': '#0d1d27', 'line-width': 4 },
    });
  }
  this.carte.set(map);
}

private readonly synchroniser = effect(() => {
  const map = this.carte();
  const data = this.traces();
  (map?.getSource('traces') as GeoJSONSource | undefined)?.setData(data);
});

Le piège de l'événement idle

Un gestionnaire branché sur idle qui réécrit une source GeoJSON relance lui-même la carte : setData émet data, la carte redessine, redevient idle, et le gestionnaire repart. La carte ne se met jamais au repos et le ventilateur de l'ordinateur portable le fait savoir. Comparez avant d'écrire : pas de changement, pas de setData.

Marqueurs, taille du conteneur et destruction

Le 23 mai 2023, MapLibre 3.0 a branché un ResizeObserver sur le conteneur de la carte. Avec l'option trackResize à sa valeur par défaut, la carte suit son conteneur, y compris un conteneur masqué en CSS qui réapparaît dans un onglet. Le ResizeObserver écrit à la main et encore recopié de tutoriel en tutoriel appelle donc resize() une seconde fois. Gardez map.resize() pour le seul cas que l'observateur ne voit pas, ou si vous passez trackResize: false.

Les marqueurs HTML sont de simples éléments du DOM. Ils ne passent pas par les gabarits Angular et c'est tant mieux : cent composants dynamiques pour cent pastilles coûtent cher à chaque cycle. Un bouton créé à la main, avec un aria-label, suffit. Seul le clic revient dans la zone, parce qu'il change l'état de la page.

carte.component.ts, les marqueurs et la destruction

readonly lieux = input<{ id: string; nom: string; coord: [number, number] }[]>([]);
readonly lieuChoisi = output<string>();
private marqueurs: Marker[] = [];

private readonly poserMarqueurs = effect(() => {
  const map = this.carte();
  const lieux = this.lieux();
  const Marker = this.maplibre?.Marker;
  if (!map || !Marker) return;
  this.marqueurs.forEach((m) => m.remove());
  this.marqueurs = lieux.map((lieu) => {
    const el = document.createElement('button');
    el.type = 'button';
    el.className = 'marqueur';
    el.setAttribute('aria-label', lieu.nom);
    el.addEventListener('click', () => this.zone.run(() => this.lieuChoisi.emit(lieu.id)));
    return new Marker({ element: el }).setLngLat(lieu.coord).addTo(map);
  });
});

private readonly nettoyer = inject(DestroyRef).onDestroy(() => {
  this.detruit = true;
  this.marqueurs.forEach((m) => m.remove());
  this.carte()?.remove();
});

Au-delà de quelques centaines de points, quittez les marqueurs HTML pour une couche circle ou symbol dans une source GeoJSON : le rendu passe sur la carte graphique.

map.remove() n'est pas une politesse. La documentation de MapLibre l'écrit : elle libère les éléments du DOM, les écouteurs, les web workers et les ressources WebGL. Le code de MapLibre le rappelle aussi : la réserve de workers préchauffée ne se vide que lorsque toutes les cartes ont été retirées par map.remove(). Une application qui navigue entre dix fiches avec carte, sans rien rendre, garde tout en mémoire.

Le même contenu, à imprimer : la checklist de mise en production d'une carte web

Recevoir la checklist

Rendu serveur : ce qui casse, et ce qui passe

Le 27 septembre 2026, nous avons importé maplibre-gl 6.11.2 sous Node 20.17, sans navigateur. L'import passe. La construction échoue : new Map() lève « document is not defined ». Le module se charge donc côté serveur sans dommage, c'est la carte qui ne peut pas y naître.

La règle tient en une ligne de la documentation d'Angular : les rappels de rendu, dont afterNextRender, ne s'exécutent ni pendant le rendu serveur ni pendant le pré-rendu. Une carte créée là est sûre par construction. Le serveur rend le conteneur vide, à la bonne hauteur, et le navigateur y pose la carte. Réservez une hauteur fixe au conteneur : sans elle, la page saute à l'arrivée de la carte et le score de stabilité visuelle en pâtit.

Pour le code qui doit décider ailleurs, un service qui précharge le style par exemple, isPlatformBrowser(inject(PLATFORM_ID)) reste la bonne garde.

un service qui ne touche au navigateur que sur le navigateur

import { Injectable, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class PrechargementCarte {
  private readonly navigateur = isPlatformBrowser(inject(PLATFORM_ID));

  precharger(): void {
    if (!this.navigateur) return;
    void import('maplibre-gl').then((m) => m.prewarm());
  }
}

Quand vous n'avez pas à écrire ce composant

Un composant MapLibre bien fait tient en une centaine de lignes. Il ne fournit ni fond de carte, ni itinéraires, ni lieux, ni recherche d'adresse : tout cela reste à servir, à héberger et à tenir à jour. Si votre besoin est de montrer des randonnées, des boucles vélo et des lieux sur une page, la carte La Trace se pose par le bloc à coller ou par le SDK, et le composant Angular se réduit à un conteneur. Elle sert aujourd'hui 134 531 itinéraires publiés, sur 2 248 cartes qui ont reçu au moins une visite depuis le 10 avril 2026.

La carte du Parc naturel régional de Chartreuse, telle qu'elle s'intègre dans une page : itinéraires, lieux et recherche d'adresse, sans composant à maintenir.

Avant de mettre la carte en production

  • Les imports de maplibre-gl sont nommés, aucun import par défaut
  • maplibre-gl-worker.mjs est copié dans les assets et setWorkerUrl() pointe dessus
  • La feuille de style maplibre-gl.css est chargée, sinon les contrôles s'empilent sous la carte
  • La carte naît dans afterNextRender, hors de la zone, avec une garde contre la destruction précoce
  • Les sources et couches sont posées sur style.load, pas sur load
  • Un seul effect() écrit dans chaque source, par setData
  • Aucun gestionnaire sur idle ne réécrit une source sans comparer
  • map.remove() est appelé dans DestroyRef.onDestroy
  • Le conteneur a une hauteur fixe côté serveur
  • L'attribution du fond de carte reste visible

FAQ

Comment utiliser MapLibre avec Angular ?

Installez maplibre-gl, déclarez sa feuille de style et son worker dans angular.json, puis créez la carte dans afterNextRender d'un composant, hors de la zone Angular. Posez sources et couches sur l'événement style.load, mettez les données à jour par setData depuis un effect(), et appelez map.remove() à la destruction.

Faut-il utiliser ngx-maplibre-gl ?

La bibliothèque @maplibre/ngx-maplibre-gl, en version 22.1.0 le 27 septembre 2026, demande Angular 22 et MapLibre 6. Elle convient bien à une carte déclarative, écrite dans le gabarit. Dès que la carte porte une logique propre, une centaine de lignes à vous se relisent mieux qu'une couche d'abstraction de plus.

Pourquoi ma carte MapLibre reste grise dans Angular ?

Trois causes, dans l'ordre : le worker introuvable depuis MapLibre 6 (vérifiez l'onglet réseau et setWorkerUrl), un conteneur sans hauteur, ou une feuille de style absente. Une erreur WebGL se lit désormais par map.on('error'), puisque WebGL 2 est exigé depuis la version 6.

MapLibre fonctionne-t-elle avec le rendu serveur d'Angular ?

Oui, à condition de ne créer la carte que dans le navigateur. L'import du module passe sous Node, la construction de la carte non. afterNextRender ne s'exécute pas côté serveur, c'est le bon endroit.

Faut-il encore NgZone.runOutsideAngular dans une application sans zone ?

Non, il ne change rien dans une application sans zone, la configuration par défaut depuis Angular 21. Il reste utile, et sans risque, dans une bibliothèque qui doit aussi tourner sous zone.js.

Sources

  1. maplibre-gl sur le registre npm, version 6.11.2, consulté le 27 septembre 2026
  2. @angular/core sur le registre npm, version 22.2.0, consulté le 27 septembre 2026
  3. CHANGELOG de MapLibre GL JS, version 6.0.0 et 3.0.0, consulté le 27 septembre 2026
  4. Guide de migration de MapLibre v5 vers v6, section setWorkerUrl, consulté le 27 septembre 2026
  5. Installation de MapLibre GL JS par outil de build, consulté le 27 septembre 2026
  6. Référence de la classe Map de MapLibre (remove, resize, trackResize), consulté le 27 septembre 2026
  7. README et licence BSD de MapLibre GL JS, consulté le 27 septembre 2026
  8. Angular, guide Zoneless, consulté le 27 septembre 2026
  9. Angular, guide des effets (effect, afterRenderEffect), consulté le 27 septembre 2026
  10. Angular, cycle de vie des composants et rappels de rendu, consulté le 27 septembre 2026
  11. Angular, système de build esbuild par défaut depuis la version 17, consulté le 27 septembre 2026
  12. @maplibre/ngx-maplibre-gl sur le registre npm, version 22.1.0, consulté le 27 septembre 2026
  13. Mesures La Trace du 27 septembre 2026 : import de maplibre-gl 6.11.2 sous Node 20.17, compilation du composant avec TypeScript en mode strict, taille compressée des modules
  14. Mesure La Trace en base, 27 septembre 2026 : sessions, cartes et itinéraires publiés
Lucas Tesnier

Lucas Tesnier

Co-fondateur de La Trace

Lucas Tesnier a cofondé La Trace en 2023 et en dirige le produit et la technique. Il écrit sur ce qu'il voit chez les offices, les parcs et les hébergeurs qui publient leurs itinéraires : l'intégration, les données, les marchés publics.

Tous ses guides · Comment nous écrivons

À emporter

La checklist de mise en production d'une carte sur un site

Consentement et cookies, clés et quotas, tuiles, géocodage, accessibilité et performance : les points à vérifier avant de mettre une carte en ligne, avec une case par point.

Merci, la checklist part dans votre boîte mail.

Ouvrir la checklist

Votre application Angular a besoin d'une carte, pas d'un chantier cartographique ?

Une démonstration de trente minutes pour voir la carte La Trace posée dans une page Angular, par le bloc à coller ou par le SDK, avec vos itinéraires et vos lieux.

Demander une démonstration
Thank you! Your submission has been received!
L'envoi n'a pas abouti. Réessayez, ou écrivez-nous à contact@latrace.com.
Les VéloroutesPour les prosMes enregistrementsMes informationsBlogNous contacterAidez-nous à nous améliorer