---
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 (
{
// 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)
}}
/>
)
}
```
## 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 {
static override type = 'my-shape' as const
static override props: RecordProps = {
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
}
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.