snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
106
apps/docs/content/sdk-features/culling.mdx
Normal file
106
apps/docs/content/sdk-features/culling.mdx
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Culling
|
||||
created_at: 12/20/2025
|
||||
updated_at: 12/29/2025
|
||||
keywords:
|
||||
- culling
|
||||
- visibility
|
||||
- viewport
|
||||
- performance
|
||||
- rendering
|
||||
status: published
|
||||
date: 12/20/2025
|
||||
readability: 9
|
||||
voice: 8
|
||||
completeness: 9
|
||||
accuracy: 9
|
||||
notes: Example link path may need verification. Could add more contractions for voice.
|
||||
---
|
||||
|
||||
The culling system optimizes rendering performance by hiding shapes that are outside the viewport.
|
||||
|
||||
Culled shapes stay in the DOM with `display: none`, so they cost nothing to render, and they stay in the store, so they can still be selected, hit-tested, and exported. The culling set is an incremental derivation that updates as the camera moves or shapes change. See [Performance](/sdk-features/performance) for how culling fits with the other rendering optimizations.
|
||||
|
||||
## Using the culling APIs
|
||||
|
||||
The editor exposes two sets of shape IDs. [Editor#getNotVisibleShapes](?) returns the shapes whose page bounds don't intersect the viewport and whose shape util allows culling. [Editor#getCulledShapes](?) removes the selected shapes and the shape being edited from that set, so users can always see what they're working with. Both are reactive, and `getCulledShapes()` returns the same `Set` instance while its contents are unchanged, so it's cheap to read in `track` components or `useValue`.
|
||||
|
||||
```tsx
|
||||
import { Tldraw } from 'tldraw'
|
||||
import 'tldraw/tldraw.css'
|
||||
|
||||
export default function CullingExample() {
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0 }}>
|
||||
<Tldraw
|
||||
onMount={(editor) => {
|
||||
// Get the IDs of shapes outside the viewport (before selection filtering)
|
||||
const notVisible = editor.getNotVisibleShapes()
|
||||
|
||||
// Get the IDs of shapes that should not render (excludes selected/editing shapes)
|
||||
const culled = editor.getCulledShapes()
|
||||
|
||||
console.log('Not visible:', notVisible.size)
|
||||
console.log('Actually culled:', culled.size)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
The first layer queries the editor's spatial index for shapes whose page bounds intersect the viewport and marks everything else as not visible, skipping shapes whose util's `canCull` returns `false`. The second layer removes the selected shapes and the editing shape, so a user can scroll a shape partly or fully out of view while still seeing and interacting with it.
|
||||
|
||||
## Shape-level control
|
||||
|
||||
A shape type opts out of culling by overriding [ShapeUtil#canCull](?). The default returns `true`. When your override returns `false`, the shape never enters the not-visible set, so it never gets `display: none`:
|
||||
|
||||
```tsx
|
||||
import { ShapeUtil, TLBaseShape, RecordProps, T, Rectangle2d } from 'tldraw'
|
||||
|
||||
type MyShape = TLBaseShape<'my-shape', { w: number; h: number; hasGlow: boolean }>
|
||||
|
||||
class MyShapeUtil extends ShapeUtil<MyShape> {
|
||||
static override type = 'my-shape' as const
|
||||
static override props: RecordProps<MyShape> = {
|
||||
w: T.number,
|
||||
h: T.number,
|
||||
hasGlow: T.boolean,
|
||||
}
|
||||
|
||||
getDefaultProps(): MyShape['props'] {
|
||||
return { w: 100, h: 100, hasGlow: false }
|
||||
}
|
||||
|
||||
getGeometry(shape: MyShape) {
|
||||
return new Rectangle2d({ width: shape.props.w, height: shape.props.h, isFilled: true })
|
||||
}
|
||||
|
||||
override canCull(shape: MyShape): boolean {
|
||||
// Shapes with glow effects shouldn't be culled because
|
||||
// the glow might be visible even when the shape bounds aren't
|
||||
if (shape.props.hasGlow) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
component(shape: MyShape) {
|
||||
return <div style={{ width: shape.props.w, height: shape.props.h }} />
|
||||
}
|
||||
|
||||
getIndicatorPath(shape: MyShape) {
|
||||
const path = new Path2D()
|
||||
path.rect(0, 0, shape.props.w, shape.props.h)
|
||||
return path
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Shapes whose util overrides `canCull` are subscribed to individually inside the culling derivation, so a `canCull` that reads many props re-runs the derivation more often. Shapes using the default fast path don't pay this cost. Disable culling only for shapes that need it: visual effects that extend past the bounds, shapes that measure their DOM, or animations that should keep running off-screen. See [Performance](/sdk-features/performance#Disable-culling-only-when-necessary) for guidance.
|
||||
|
||||
## Related examples
|
||||
|
||||
- [Size from DOM](/examples/shapes/tools/size-from-dom) - A shape that disables culling because it measures its DOM element to determine size.
|
||||
Reference in New Issue
Block a user