# Virtualisierung und Performance

> Wie SuperScheduler Zeilen, Zellen und Ereignisse ohne React-Renderings virtualisiert, was in Ihrer Integration Zeit kostet, eine Checkliste und wie Sie messen.

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

Beide Editionen virtualisieren in zwei Dimensionen: Nur die Zeilen und Tage rund um den sichtbaren Bereich haben DOM-Knoten, und Scrollen, Zoomen und Ziehen aktualisieren dieses DOM direkt, ohne React-Renderings. In einer Integration fließt die Zeit in Ihre Render-Hooks, React-Render-Slots, die Datenaufbereitung und Props, deren Identität sich bei jedem Rendern ändert. Beschränken Sie Hooks auf günstige Nachschlagevorgänge, halten Sie Props und Ereignisobjekte stabil und messen Sie Produktions-Builds mit gedrosselter CPU.

SuperScheduler ist für Pläne mit Tausenden von Zeilen und Hunderttausenden von Ereignissen gebaut. Die Engine ist ein imperativer DOM-Renderer: React beherbergt sie, und Ihr React-Baum rendert nur, wenn sich Ihre eigenen Props oder Ihr eigener State ändern. Diese Anleitung erklärt, was die Engine für Sie übernimmt, wo eine Integration trotzdem Zeit verbrauchen kann und wie Sie ehrlich messen.

Die Virtualisierung gilt für beide Editionen. Lite virtualisiert Zeilen, Tage und Ereignisse mit denselben Kernindizes und rendert beim Scrollen nie React. Render-Hooks, Zoom, Detailstufen und React-Render-Slots sind Pro-Funktionen; die Abschnitte dazu gelten daher nur für Pro.

## Wie die Virtualisierung funktioniert
### Das gemountete Fenster
Nur ein Fenster rund um den sichtbaren Bereich hat DOM-Knoten: der sichtbare Bereich plus ein Rand, eingerastet auf Abschnitte von einem Viertel des sichtbaren Bereichs, mit zwei Abschnitten Rand auf jeder Seite. Das Fenster wird bei jedem Scroll-Ereignis neu berechnet, ändert sich aber nur, wenn eine Abschnittsgrenze überschritten wird. In einem Scroll-Frame werden der sichtbare Bereich plus ein Abschnitt synchron gezeichnet; der Rest des Fensters wird schrittweise in den folgenden Frames gezeichnet, die nächstgelegenen Zeilen zuerst. Aufrufe wie `control.update()` rendern synchron und werden nie auf mehrere Frames verteilt.

### Zeilen
Ressourcen werden einmal zu Zeilen abgeflacht (Baumzeilen eingeschlossen), und die Zeilenhöhen liegen in einem Präfixsummen-Index; die Zeilen für eine Scrollposition zu finden ist daher logarithmisch und kein Durchlauf über alle Zeilen. Zuklappen, Aufklappen oder Filtern baut die Liste der sichtbaren Zeilen in einem Durchgang neu auf. Ändert sich die Höhe einer Zeile, werden die Zeilen darunter als ganze Blöcke verschoben, statt jede Zelle und jedes Ereignis neu zu stylen.

### Zellen
Rasterlinien und die Schattierung von Wochenenden oder Nicht-Geschäftszeit werden als sich wiederholender Hintergrund gezeichnet; gewöhnliche Zellen haben also überhaupt keine DOM-Knoten. Eine Zelle erhält nur dann einen Knoten, wenn sie angepasst ist (durch `onBeforeCellRender` oder `renderCell`) und im gemounteten Fenster liegt. Wenn Sie unter 2 px pro Zelle herauszoomen, werden angepasste Zellen nicht erzeugt, und ihr Hook wird nicht aufgerufen.

### Ereignisse
Ereignisse werden pro Ressource nach Zeit indiziert, sodass ein sichtbarer Zeitraum mit einer logarithmischen Abfrage gefunden wird. Das Stapeln bei Überlappung wird aus Zeiten berechnet, nicht aus Pixeln; Zoomen stapelt Zeilen daher nie neu. Ereignisknoten stammen aus einem Pool und werden wiederverwendet, während sich das Fenster bewegt. Bei kleinen Größen schaltet die Detailstufe Ereignisse von vollem Inhalt auf Text, auf einfache Blöcke und schließlich auf einen Balken pro Zeile um und blendet Verknüpfungen aus, deren Enden zu klein sind, um sie zu sehen. `lod: false` schaltet diese Anpassung ab und kostet beim Herauszoomen mehr.

### Keine React-Renderings während der Interaktion
Frames beim Scrollen, Zoomen, Hovern, Auswählen und Ziehen verarbeitet die Engine mit zwischengespeicherter Geometrie und einem einzigen Frame-Scheduler, der Layout-Lesezugriffe von DOM-Schreibzugriffen trennt. React-Inhalte aus `super-scheduler/react-render` sind bewusst die einzige Ausnahme: Der HTML- oder Text-Fallback wird zuerst gezeichnet, und die React-Inhalte werden nach dem Ende der Interaktion veröffentlicht, in Bündeln mit einem Zielwert von 8 ms. Optionale Funktionen wie Tastaturnavigation, Menüs und Bubbles werden als separate Chunks geladen, wenn Sie sie konfigurieren oder zum ersten Mal verwenden, nie während einer Geste.

→ https://superscheduler.org/de/examples/port-berths/
## Was in einer Integration Zeit kostet
Die Bibliothek kann Ihre Callbacks nicht günstiger machen. An diesen Stellen verbringen echte Integrationen ihre Zeit.

### Render-Hooks
| Hook | Wann er läuft | Zwischengespeichert, bis |
|---|---|---|
| `onBeforeEventRender` | Für jedes Ereignis, das das Layout braucht, nicht nur die sichtbaren, weil er `height`, `line` oder `hidden` ändern kann | Sich die Daten des Ereignisses ändern |
| `onBeforeCellRender` | Für jede Zelle im gemounteten Fenster, ab 2 px pro Zelle | Neue `resources`, `control.update()` oder eine Änderung an den Ereignissen der Zeile, wenn die Ressource `cellsAutoUpdated: true` hat |
| `onBeforeRowHeaderRender` | Für gemountete Zeilenköpfe | Sich die Zeile oder die Daten ihrer Ressource ändern |
| `onBeforeTimeHeaderRender` | Für gemountete Zellen im Zeitkopf | Sich die Zeitachse ändert (Skala, Zoomstufe, Datumsbereich) |
| `onEventMoving`, `onEventResizing`, `onTimeRangeSelecting` | Bei jeder Änderung des Schattens einer Geste | Nie zwischengespeichert |

Eine neue Funktion für einen Render-Hook leert ebenfalls dessen Cache. Beschränken Sie Hooks auf Nachschlagen und das Zusammensetzen von Strings. Berechnen Sie Maps und Sets außerhalb des Hooks vor, erzeugen Sie `Intl`-Formatter einmal auf Modulebene, lesen Sie nie Layout (`getBoundingClientRect`) und setzen Sie nie React-State innerhalb eines Hooks. Während eines Ziehvorgangs wird `args.conflicts` beim ersten Zugriff berechnet; lesen Sie es also nicht, wenn die Regel es nicht braucht.

### Props, deren Identität sich ändert
Die React-Komponente leitet nur die Props weiter, deren Identität sich seit dem letzten Rendern geändert hat (`Object.is`). Jede weitergeleitete Prop hat ihren Preis:

- Ein neues `events`-Array, dessen Objekte sich von denen des Controls unterscheiden, lädt den gesamten Ereignisspeicher neu und führt `onBeforeEventRender` erneut für jedes Ereignis aus. Wenn Sie dieselben Objekte zurückgeben (`[...args.events]` aus `onEventsChange`), wird das als Echo erkannt und das Neuladen übersprungen.
- Ein neues `resources`-Array baut die Zeilen neu auf und macht jede Zelle ungültig.
- Eine neue Hook-Funktion macht den Cache dieses Hooks ungültig.
- Neue Objekte für `timeHeaders`, `zoomLevels`, `classNames` oder `styles` werden erneut angewendet.

Definieren Sie Konstanten auf Modulebene, memoisieren Sie abgeleitete Props mit `useMemo` und hüllen Sie Handler, die von State abhängen, in `useCallback`. Eine Prop, die zwischen zwei Renderings verschwindet, fällt auf ihren Standardwert zurück; halten Sie daher auch bedingte Props stabil.

```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}
    />
  )
}
```
### React-Render-Slots
`renderEvent`, `renderRowHeader` und die anderen Render-Props mounten React-Inhalte über Portale in Knoten der Engine. Für Ereignisse und Köpfe ist das unproblematisch. `renderCell` mountet einen React-Slot pro gemounteter Zelle, was sich in einem dichten Raster schnell summiert; bevorzugen Sie bei großen Rastern Strings aus `onBeforeCellRender` und behalten Sie React den Zellen vor, die Interaktion brauchen. Memoisieren Sie die Render-Funktionen und justieren Sie `renderOptions.retain` (behaltene, abgelöste Elemente; Standard ist der kleinere Wert von 2.000 oder dem Doppelten der gemounteten Anzahl) und `renderOptions.sliceMs` (Zielwert pro Bündel, Standard 8 ms) erst nach einer Messung. Siehe [React-Render-Slots](https://superscheduler.org/de/docs/react-render-slots/).

### Datenänderungen und Ihr eigener State
- `control.events.add`, `update` und `remove` arbeiten inkrementell und eignen sich für einzelne Bearbeitungen. Für Hunderte von Änderungen auf einmal, etwa einen Import oder eine Aktualisierung vom Server, übergeben Sie dem Planer ein einziges neues Array, statt sie in einer Schleife aufzurufen.
- `control.update()` ohne Argumente ist eine vollständige Aktualisierung. Übergeben Sie nur die Optionen, die sich geändert haben.
- `onZoom` läuft in jedem Frame einer Zoomgeste. Schreiben Sie Rückmeldungen pro Frame direkt ins DOM und aktualisieren Sie React-State erst, wenn `args.phase === 'end'` gilt.
- `useScheduler({ track: [...] })` aus `super-scheduler/hooks` veröffentlicht, nachdem sich Änderungen gesetzt haben, nie pro Frame. Verfolgen Sie nur die Themen, die eine Komponente anzeigt.
- Zugeklappte Elternelemente im Baum verringern die Mount-Arbeit: Das Layout wird für aufgeklappte Zeilen berechnet.

## Checkliste
- Importieren Sie den Planer in den Routen, die ihn verwenden, damit sein Code dem Rest Ihrer App fernbleibt ([Server-Rendering](https://superscheduler.org/de/docs/ssr-prerender/#client-mount)).
- Halten Sie `events`, `resources`, `timeHeaders`, `zoomLevels`, `classNames` und Hooks zwischen Renderings stabil.
- Wandeln Sie Serverzeilen einmal pro Antwort in Ereignisobjekte um, nicht während des Renderns.
- Geben Sie dem Control eine eigene Kopie des Ereignis-Arrays (`useMemo(() => events.slice(), [events])`), weil es dieses Array direkt per splice verändert.
- Beschränken Sie `onBeforeEventRender` und `onBeforeCellRender` auf Nachschlagevorgänge; setzen Sie `cellsAutoUpdated` nur für Zeilen, deren Zellen von ihren Ereignissen abhängen.
- Bevorzugen Sie in dichten Rastern String-Hooks gegenüber `renderCell`.
- Fassen Sie große Datenänderungen in einem neuen Array zusammen.
- Laden Sie lange Zeitleisten bereichsweise mit [`super-scheduler/ranges`](https://superscheduler.org/de/docs/range-loading/), statt Daten aus mehreren Jahren zu senden.
- Lassen Sie `lod` eingeschaltet, sofern Sie nicht auf jeder Zoomstufe eine wörtliche Darstellung brauchen.
- Setzen Sie nie React-State aus Callbacks, die pro Frame laufen.

## Messen
Die Bibliothek wird mit einer reproduzierbaren Methode gemessen, und dieselbe Methode funktioniert für Ihre Integration:

- **Produktions-Builds.** Entwicklungs-Builds von React und Ihrer App sind langsamer und fügen Prüfungen hinzu.
- **Getrennte Phasen.** Erzeugen oder laden Sie die Daten vor dem Mount und messen Sie dann die Mount-Zeit mit einem anschließend erzwungenen Layout.
- **Frames, keine Durchschnitte.** Erfassen Sie die Frame-Zeiten p50, p95 und p99 beim Scrollen mit dem Mausrad über dem Raster, diagonal, während des automatischen Scrollens und bei mehreren Zoombreiten. Zählen Sie die gemounteten DOM-Knoten und den Speicher nach der Garbage Collection.
- **React-Commits.** Hüllen Sie den Planer in einen `<Profiler>` und prüfen Sie, dass Scrollen, Zoomen und Ziehen keine Commits auslösen. `onRender` feuert in Entwicklungs-Builds; in Produktion braucht es den Profiling-Build von React.
- **Gedrosselte CPU.** Wiederholen Sie die Messung mit 4-facher CPU-Drosselung in den Performance-Werkzeugen des Browsers und auf den Geräten Ihrer Nutzer.
- **Isolieren Sie Ihre Callbacks.** Vergleichen Sie keinen Hook, einen leeren Hook und Ihren Hook, um zu sehen, was Ihr Code kostet.
- **Mehrere Durchläufe.** Ein einzelner langsamer Messwert nahe einem Schwellenwert ist Rauschen. Vergleichen Sie Mediane mehrerer Durchläufe auf einer ansonsten unbelasteten Maschine.

`super-scheduler/datasets` erzeugt dieselben deterministischen Szenarien, die die Bibliothek verwendet, sodass Sie eine Last ohne Ihr Backend reproduzieren können:

```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>
    </>
  )
}
```
Sie sollten sehen, dass der Zähler nach dem ersten Mount stehen bleibt: Das Scrollen durch die 1.000 Zeilen und zwei Jahre des Plans fügt keine React-Commits hinzu.

### Veröffentlichte Messungen
Die Performance-Überprüfung der Bibliothek vom 2026-10-07 hat diese Ergebnisse festgehalten. Methode: Chromium 145 headless mit Software-Rasterisierung, Viewport 1440 × 900 bei einem Device Pixel Ratio von 1, die Demo der Bibliothek als Produktions-Build mit dem Profiling-Build von React, auf einem Apple M5 Pro mit 24 GiB, auf dem andere Anwendungen liefen. Frame-Zeiten sind Intervalle von `requestAnimationFrame` auf einem Display mit etwa 120 Hz während der Scroll-Abläufe des Messaufbaus; 8,3 ms ist also das eigene Frame-Intervall des Displays. Der Mount von S1 ist der Median aus drei Mounts; schwerere Szenarien wurden einmal gemountet.

| Szenario | Daten | Mount | Frame p50 / p95 / p99 | DOM-Knoten | Heap nach GC |
|---|---|---|---|---|---|
| S1 | 120 Zeilen, 730 Tage, etwa 6.000 Ereignisse | 23,2 ms | 8,3 / 9,1 / 9,3 ms | 2.506 | 11,2 MiB |
| S1, CPU 4× | Dieselben | 104,6 ms | 16,1 / 25,2 / 25,9 ms | 2.506 | 11,2 MiB |
| S3 | 5.000 Zeilen, 1.500 Tage, 200.021 Ereignisse | 291,6 ms | 8,3 / 9,2 / 9,4 ms | 2.035 | 205,4 MiB |
| S3Dense | Wie S3, ohne freie Nacht in irgendeinem Zimmer, 1.634.510 Ereignisse | 2.174,8 ms | 8,3 / 9,3 / 16,8 ms | 3.738 | 1.589,2 MiB |

Jedes Szenario verzeichnete null React-Renderings beim Scrollen. Diese Zahlen stammen von einer Maschine an einem Tag, mit der eigenen Demo-Anwendung der Bibliothek und ihren Hooks; sie sind Beobachtungen, keine Garantien für Ihre Integration.

## Grenzen
Das DOM bleibt bei jeder Größe klein, aber Mount-Zeit und Speicher wachsen mit der Gesamtzahl der Ereignisse, weil beim Aufbau eines Rasters das Layout für jede aufgeklappte Zeile berechnet wird. Die Zeile S3Dense oben zeigt die Obergrenze: über zwei Sekunden für den Mount und etwa 1,6 GiB Heap für 1,6 Millionen Ereignisse. Laden Sie lange vorher bereichsweise und behalten Sie nur die Tage, mit denen Menschen arbeiten. Eine API für Massenänderungen gibt es noch nicht; sehr große Live-Aktualisierungen wenden Sie daher am besten als ein einziges neues Ereignis-Array an. Viele Verknüpfungen erhöhen außerdem den Zeichenaufwand, da jeder Zeichenvorgang jede Verknüpfung berücksichtigt.

Verwandte Anleitungen: [React-Integration](https://superscheduler.org/de/docs/react-integration/), [Kontrollierter Zustand](https://superscheduler.org/de/docs/controlled-state/), [Zeitskalen und Zoom](https://superscheduler.org/de/docs/time-scales-zoom/) und [Fehlerbehebung](https://superscheduler.org/de/docs/troubleshooting/).
