# Virtualisation et performances

> Comment SuperScheduler virtualise lignes, cellules et événements sans rendu React, ce qui coûte du temps dans une intégration, une liste de contrôle et la mesure.

Source: https://superscheduler.org/fr/docs/performance-virtualization/
Reviewed: 2026-10-07

Les deux éditions virtualisent dans les deux dimensions : seules les lignes et les dates autour de la zone visible ont des nœuds DOM, et le défilement, le zoom et le glisser mettent ce DOM à jour directement, sans rendu React. Dans une intégration, le temps part dans vos hooks de rendu, les slots de rendu React, la transformation des données et les props dont l’identité change à chaque rendu. Limitez les hooks à des recherches peu coûteuses, gardez props et objets événements stables, et mesurez des builds de production avec bridage du CPU.

SuperScheduler est conçu pour des plannings de milliers de lignes et de centaines de milliers d’événements. Le moteur effectue un rendu DOM impératif : React l’héberge, et votre arbre React ne se rend que lorsque vos propres props ou votre état changent. Ce guide explique ce que le moteur fait pour vous, où une intégration peut encore perdre du temps, et comment mesurer honnêtement.

La virtualisation s’applique aux deux éditions. Lite virtualise lignes, jours et événements avec les mêmes index internes et ne déclenche jamais de rendu React pendant le défilement. Les hooks de rendu, le zoom, le niveau de détail et les slots de rendu React sont des fonctions Pro : les sections qui en parlent ne concernent donc que Pro.

## Fonctionnement de la virtualisation
### La fenêtre montée
Seule une fenêtre autour de la zone visible a des nœuds DOM : la zone visible plus une marge, alignée sur des blocs d’un quart de la zone visible, avec deux blocs de marge de chaque côté. La fenêtre est recalculée à chaque événement de défilement mais ne change que lorsqu’une limite de bloc est franchie. Sur une image de défilement, la zone visible plus un bloc sont peints de façon synchrone ; le reste de la fenêtre est peint progressivement sur les images suivantes, lignes les plus proches d’abord. Les appels comme `control.update()` font un rendu synchrone et ne sont jamais étalés sur plusieurs images.

### Les lignes
Les ressources sont aplaties en lignes une seule fois (lignes d’arborescence comprises), et les hauteurs de lignes vivent dans un index de sommes préfixes : trouver les lignes correspondant à une position de défilement est logarithmique, sans parcours de toutes les lignes. Replier, déplier ou filtrer reconstruit la liste des lignes visibles en une passe. Une ligne dont la hauteur change décale les lignes situées en dessous par tranches entières, au lieu de restyler chaque cellule et chaque événement.

### Les cellules
Les lignes de grille et l’ombrage des week-ends ou du temps non ouvré sont peints comme un arrière-plan répété : les cellules ordinaires n’ont donc aucun nœud DOM. Une cellule ne reçoit un nœud que si elle est personnalisée (par `onBeforeCellRender` ou `renderCell`) et se trouve dans la fenêtre montée. Quand vous dézoomez en dessous de 2 px par cellule, les cellules personnalisées ne sont pas créées et leur hook n’est pas appelé.

### Les événements
Les événements sont indexés par ressource et par temps : une plage visible se trouve par une requête logarithmique. L’empilement des chevauchements est calculé à partir des horaires, pas des pixels : zoomer ne réempile donc jamais les lignes. Les nœuds d’événements proviennent d’un pool et sont réutilisés à mesure que la fenêtre se déplace. Aux petites tailles, le niveau de détail fait passer les événements du contenu complet au texte, puis à de simples blocs et enfin à une barre par ligne, et masque les liens dont les extrémités sont trop petites pour être vues. `lod: false` désactive cette adaptation et coûte plus cher en vue dézoomée.

### Aucun rendu React pendant l’interaction
Les images de défilement, de zoom, de survol, de sélection et de glissement sont gérées par le moteur, avec une géométrie en cache et un ordonnanceur d’images unique qui sépare les lectures de mise en page des écritures dans le DOM. Le contenu React de `super-scheduler/react-render` est la seule exception, par conception : le repli HTML ou texte est peint en premier, et le contenu React est publié après la fin de l’interaction, par lots qui visent 8 ms. Les fonctions optionnelles comme la navigation au clavier, les menus et les bulles se chargent sous forme de chunks séparés quand vous les configurez ou les utilisez pour la première fois, jamais pendant un geste.

→ https://superscheduler.org/fr/examples/port-berths/
## Ce qui coûte du temps dans une intégration
La bibliothèque ne peut pas rendre vos callbacks moins coûteux. Voici les endroits où les intégrations réelles perdent du temps.

### Les hooks de rendu
| Hook | Quand il s’exécute | En cache jusqu’à |
|---|---|---|
| `onBeforeEventRender` | Pour chaque événement dont la mise en page a besoin, pas seulement les visibles, car il peut modifier `height`, `line` ou `hidden` | Un changement des données de l’événement |
| `onBeforeCellRender` | Pour chaque cellule de la fenêtre montée, au-dessus de 2 px par cellule | De nouvelles `resources`, `control.update()`, ou un changement des événements de la ligne quand la ressource a `cellsAutoUpdated: true` |
| `onBeforeRowHeaderRender` | Pour les en-têtes de ligne montés | Un changement de la ligne ou des données de sa ressource |
| `onBeforeTimeHeaderRender` | Pour les cellules d’en-tête montées | Un changement de l’axe du temps (échelle, niveau de zoom, dates) |
| `onEventMoving`, `onEventResizing`, `onTimeRangeSelecting` | À chaque changement de l’ombre pendant un geste | Jamais mis en cache |

Passer une nouvelle fonction pour un hook de rendu vide aussi son cache. Limitez les hooks à des recherches et à la construction de chaînes. Précalculez maps et sets hors du hook, créez les formateurs `Intl` une seule fois au niveau du module, ne lisez jamais la mise en page (`getBoundingClientRect`) et ne modifiez jamais d’état React dans un hook. Pendant un glissement, `args.conflicts` est calculé au premier accès : ne le lisez donc pas si la règle n’en a pas besoin.

### Les props dont l’identité change
Le composant React ne transmet que les props dont l’identité a changé depuis le dernier rendu (`Object.is`). Chaque prop transmise a un coût :

- Un nouveau tableau `events` dont les objets diffèrent de ceux du contrôle recharge tout le store d’événements et réexécute `onBeforeEventRender` pour chaque événement. Renvoyer les mêmes objets (`[...args.events]` depuis `onEventsChange`) est reconnu comme un écho et évite le rechargement.
- Un nouveau tableau `resources` reconstruit les lignes et invalide toutes les cellules.
- Une nouvelle fonction de hook invalide le cache de ce hook.
- De nouveaux objets `timeHeaders`, `zoomLevels`, `classNames` ou `styles` sont appliqués à nouveau.

Définissez les constantes au niveau du module, mémoïsez les props dérivées avec `useMemo`, et enveloppez dans `useCallback` les handlers qui dépendent de l’état. Une prop qui disparaît entre deux rendus revient à sa valeur par défaut : gardez donc aussi les props conditionnelles stables.

```tsx
// src/StablePlanning.tsx
import { useCallback, useMemo, useState } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type {
  SchedulerBeforeCellRenderArgs,
  SchedulerBeforeEventRenderArgs,
  SchedulerEventsChangeArgs,
} from 'super-scheduler'

type Stay = { status: 'confirmed' | 'tentative'; guests: number }

// Module scope: created once for the life of the page.
const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [
  { groupBy: 'Month' },
  { groupBy: 'Day', format: 'd' },
]
const STATUS_CLASS = {
  confirmed: 'stay stay--confirmed',
  tentative: 'stay stay--tentative',
} as const

// Runs for every event the layout needs, then is cached per event: keep it to lookups and strings.
function onBeforeEventRender(args: SchedulerBeforeEventRenderArgs) {
  const data = args.data as SuperScheduler.EventRenderData<Stay>
  data.cssClass = STATUS_CLASS[data.status]
  data.html = `${SuperScheduler.Util.escapeHtml(data.text)} <small>${data.guests}</small>`
}

interface Props {
  readonly rooms: SuperScheduler.ResourceData[]
  readonly initial: SuperScheduler.EventData<Stay>[]
  /** Days the hotel is closed, as `yyyy-MM-dd`. */
  readonly closedDays: readonly string[]
}

export function StablePlanning({ rooms, initial, closedDays }: Props) {
  // Map server data to event objects once; new objects on every render would reload the store.
  const [events, setEvents] = useState(initial)
  const owned = useMemo(() => events.slice(), [events])

  // A Set built when its input changes, so the cell hook is a constant-time lookup.
  const closed = useMemo(() => new Set(closedDays), [closedDays])
  const onBeforeCellRender = useCallback(
    (args: SchedulerBeforeCellRenderArgs) => {
      if (closed.has(args.cell.start.toString('yyyy-MM-dd'))) args.cell.properties.disabled = true
    },
    [closed],
  )

  // The same objects handed back are recognized as an echo: no reload, no repaint.
  const onEventsChange = useCallback((args: SchedulerEventsChangeArgs) => {
    setEvents([...args.events] as SuperScheduler.EventData<Stay>[])
  }, [])

  return (
    <SuperSchedulerComponent
      startDate="2026-01-01"
      days={365}
      scale="Day"
      cellWidth={40}
      timeHeaders={TIME_HEADERS}
      resources={rooms}
      events={owned}
      onEventsChange={onEventsChange}
      onBeforeEventRender={onBeforeEventRender}
      onBeforeCellRender={onBeforeCellRender}
    />
  )
}
```
### Les slots de rendu React
`renderEvent`, `renderRowHeader` et les autres props de rendu montent du contenu React via des portails dans les nœuds du moteur. Ils conviennent bien aux événements et aux en-têtes. `renderCell` monte un slot React par cellule montée, ce qui s’accumule vite sur une grille dense ; préférez les chaînes de `onBeforeCellRender` pour les grandes grilles et réservez React aux cellules qui ont besoin d’interaction. Mémoïsez les fonctions de rendu, et n’ajustez `renderOptions.retain` (éléments détachés conservés ; par défaut, le plus petit de 2 000 ou du double du nombre d’éléments montés) et `renderOptions.sliceMs` (durée visée par lot, 8 ms par défaut) qu’après avoir mesuré. Voir [Slots de rendu React](https://superscheduler.org/fr/docs/react-render-slots/).

### Les changements de données et votre propre état
- `control.events.add`, `update` et `remove` sont incrémentaux et conviennent aux modifications isolées. Pour des centaines de modifications d’un coup, comme un import ou un rafraîchissement depuis le serveur, passez au planificateur un seul nouveau tableau au lieu de les appeler en boucle.
- `control.update()` sans arguments est un rafraîchissement complet. Ne passez que les options qui ont changé.
- `onZoom` s’exécute à chaque image d’un geste de zoom. Écrivez le retour visuel image par image dans le DOM, et ne mettez à jour l’état React que lorsque `args.phase === 'end'`.
- `useScheduler({ track: [...] })` de `super-scheduler/hooks` publie une fois les changements stabilisés, jamais à chaque image. Ne suivez que les sujets qu’un composant affiche.
- Les parents d’arborescence repliés réduisent le travail de montage : la mise en page est calculée pour les lignes dépliées.

## Liste de vérification
- Importez le planificateur sur les routes qui l’utilisent, pour que son code reste hors du reste de votre application ([Rendu serveur](https://superscheduler.org/fr/docs/ssr-prerender/#client-mount)).
- Gardez `events`, `resources`, `timeHeaders`, `zoomLevels`, `classNames` et les hooks stables d’un rendu à l’autre.
- Transformez les lignes renvoyées par le serveur en objets événements une fois par réponse, pas pendant le rendu.
- Donnez au contrôle sa propre copie du tableau d’événements (`useMemo(() => events.slice(), [events])`), car il modifie ce tableau sur place avec splice.
- Limitez `onBeforeEventRender` et `onBeforeCellRender` à des recherches ; ne définissez `cellsAutoUpdated` que sur les lignes dont les cellules dépendent de leurs événements.
- Préférez les hooks qui renvoient des chaînes à `renderCell` sur les grilles denses.
- Regroupez les gros changements de données en un seul nouveau tableau.
- Chargez les longues frises par plages avec [`super-scheduler/ranges`](https://superscheduler.org/fr/docs/range-loading/) au lieu d’envoyer des années de données.
- Laissez `lod` activé, sauf si vous avez besoin d’un rendu littéral à tous les niveaux de zoom.
- Ne modifiez jamais l’état React depuis des callbacks appelés à chaque image.

## Mesurer
La bibliothèque est mesurée avec une méthode reproductible, et cette même méthode fonctionne pour votre intégration :

- **Builds de production.** Les builds de développement de React et de votre application sont plus lents et ajoutent des vérifications.
- **Phases séparées.** Générez ou récupérez les données avant le montage, puis mesurez le temps de montage avec une mise en page forcée juste après.
- **Des images, pas des moyennes.** Relevez les p50, p95 et p99 du temps par image en faisant défiler à la molette sur la grille, en diagonale, pendant le défilement automatique et à plusieurs largeurs de zoom. Comptez les nœuds DOM montés et la mémoire après le ramasse-miettes.
- **Commits React.** Enveloppez le planificateur dans un `<Profiler>` et vérifiez que le défilement, le zoom et le glissement ne provoquent aucun commit. `onRender` se déclenche dans les builds de développement ; en production, il nécessite le build de profilage de React.
- **CPU bridé.** Recommencez avec un bridage du CPU à 4× dans les outils de performance du navigateur, et sur les appareils de vos utilisateurs.
- **Isolez vos callbacks.** Comparez l’absence de hook, un hook vide et votre hook pour voir ce que coûte votre code.
- **Répétez les mesures.** Un seul échantillon lent près d’un seuil, c’est du bruit. Comparez les médianes de plusieurs exécutions sur une machine par ailleurs inactive.

`super-scheduler/datasets` génère les mêmes scénarios déterministes que ceux qu’utilise la bibliothèque, pour reproduire une charge sans votre backend :

```tsx
// src/ScrollProfile.tsx
import { Profiler, useRef, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { generateScenario, toSuperSchedulerData } from 'super-scheduler/datasets'

// Deterministic data generated before mounting, so generation is not measured as mount time.
// S2: 1,000 rows over 730 days from 2026-01-01, about 40,000 events. Same seed, same data.
const S2 = toSuperSchedulerData(generateScenario('S2'))
type DatasetResource = (typeof S2.resources)[number]

// The generator's resource type is an interface without an index signature, so it is not directly
// assignable to ResourceData: copy the fields the scheduler needs instead of casting.
const toResource = (resource: DatasetResource): SuperScheduler.ResourceData => ({
  id: resource.id,
  name: resource.name,
  ...(resource.expanded === undefined ? {} : { expanded: resource.expanded }),
  ...(resource.frozen === undefined ? {} : { frozen: resource.frozen }),
  ...(resource.children === undefined ? {} : { children: resource.children.map(toResource) }),
})
const RESOURCES = S2.resources.map(toResource)

export function ScrollProfile() {
  const [events] = useState(() => S2.events.slice())
  const commits = useRef(0)
  const counter = useRef<HTMLOutputElement>(null)

  // Written straight to the DOM: a state update here would itself cause the renders we count.
  const onRender = () => {
    commits.current += 1
    if (counter.current !== null) counter.current.textContent = `${commits.current} React commits`
  }

  return (
    <>
      <output ref={counter}>0 React commits</output>
      <Profiler id="planning" onRender={onRender}>
        <SuperSchedulerComponent
          startDate="2026-01-01"
          days={730}
          scale="Day"
          cellWidth={32}
          treeEnabled
          heightSpec="Fixed"
          height={640}
          resources={RESOURCES}
          events={events}
        />
      </Profiler>
    </>
  )
}
```
Vous devriez voir le compteur s’arrêter après le montage initial : faire défiler les 1 000 lignes et les deux années du planning n’ajoute aucun commit React.

### Mesures publiées
La revue de performances de la bibliothèque du 2026-10-07 a enregistré ces résultats. Méthode : Chromium 145 headless avec rastérisation logicielle, zone d’affichage de 1440 × 900 avec un device pixel ratio de 1, la démo de la bibliothèque en build de production avec le build de profilage de React, sur un Apple M5 Pro de 24 Gio qui faisait tourner d’autres applications. Les temps par image sont des intervalles de `requestAnimationFrame` sur un écran d’environ 120 Hz pendant les parcours de défilement du banc de test : 8,3 ms correspond donc à l’intervalle d’image de l’écran lui-même. Le montage de S1 est la médiane de trois montages ; les scénarios plus lourds ont été montés une seule fois.

| Scénario | Données | Montage | Image p50 / p95 / p99 | Nœuds DOM | Tas après GC |
|---|---|---|---|---|---|
| S1 | 120 lignes, 730 jours, environ 6 000 événements | 23,2 ms | 8,3 / 9,1 / 9,3 ms | 2 506 | 11,2 Mio |
| S1, CPU 4× | Identiques | 104,6 ms | 16,1 / 25,2 / 25,9 ms | 2 506 | 11,2 Mio |
| S3 | 5 000 lignes, 1 500 jours, 200 021 événements | 291,6 ms | 8,3 / 9,2 / 9,4 ms | 2 035 | 205,4 Mio |
| S3Dense | Comme S3, sans aucune nuit libre dans aucune chambre, 1 634 510 événements | 2 174,8 ms | 8,3 / 9,3 / 16,8 ms | 3 738 | 1 589,2 Mio |

Chaque scénario a enregistré zéro rendu React pendant le défilement. Ces chiffres proviennent d’une seule machine, un seul jour, avec l’application de démonstration de la bibliothèque et ses hooks ; ce sont des observations, pas des garanties pour votre intégration.

## Limites
Le DOM reste petit quelle que soit la taille, mais le temps de montage et la mémoire augmentent avec le nombre total d’événements, car la mise en page est calculée pour chaque ligne dépliée lors de la construction de la grille. La ligne S3Dense ci-dessus montre le plafond : plus de deux secondes de montage et environ 1,6 Gio de tas pour 1,6 million d’événements. Bien avant cela, chargez par plages et ne gardez que les dates sur lesquelles on travaille. Il n’existe pas encore d’API de mutation en masse : les très grosses mises à jour en direct s’appliquent donc de préférence sous forme d’un seul nouveau tableau d’événements. Les liens nombreux ajoutent aussi du travail de peinture, car chaque peinture prend en compte tous les liens.

Guides associés : [Intégration React](https://superscheduler.org/fr/docs/react-integration/), [État contrôlé](https://superscheduler.org/fr/docs/controlled-state/), [Échelles de temps et zoom](https://superscheduler.org/fr/docs/time-scales-zoom/) et [Dépannage](https://superscheduler.org/fr/docs/troubleshooting/).
