Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/fix-popper-perf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@zag-js/popper": patch
"@zag-js/popover": patch
---

Improve performance by reducing the number of style recalculations when scrolling with heavy content. Add
`sizeMiddleware` positioning option to optionally disable the size middleware for better scroll performance when not
using `sameWidth` or `fitViewport`.
139 changes: 139 additions & 0 deletions examples/next-ts/pages/popover-perf.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import * as popover from "@zag-js/popover"
import { normalizeProps, Portal, useMachine } from "@zag-js/react"
import { useId } from "react"
import { Presence } from "../components/presence"

// Generate lots of items to make the popover content heavy
const items = Array.from({ length: 200 }, (_, i) => ({
id: i,
title: `Item ${i + 1}`,
description: `This is a detailed description for item ${i + 1} that adds more DOM nodes and content weight.`,
}))

// Generate tall page content to enable scrolling
const pageContent = Array.from({ length: 50 }, (_, i) => `Section ${i + 1}`)

export default function Page() {
const service = useMachine(popover.machine, {
id: useId(),
positioning: { placement: "right-start", sizeMiddleware: false },
})

const api = popover.connect(service, normalizeProps)

return (
<main style={{ padding: 40 }}>
<h1>Popover Performance Test</h1>
<p style={{ marginBottom: 16, color: "#666" }}>
Open the popover, then scroll the page. Watch for sluggish position updates in DevTools Performance tab.
</p>

<div
style={{
position: "sticky",
top: 0,
background: "white",
padding: "12px 0",
zIndex: 10,
borderBottom: "1px solid #eee",
}}
>
<button {...api.getTriggerProps()} style={{ padding: "8px 16px", fontSize: 16 }}>
Open heavy popover
</button>

<Portal>
<div {...api.getPositionerProps()}>
<Presence
{...api.getContentProps()}
style={{
background: "white",
border: "1px solid #ccc",
borderRadius: 8,
boxShadow: "0 4px 24px rgba(0,0,0,0.12)",
width: 380,
maxHeight: 500,
overflow: "auto",
padding: 16,
}}
>
<div {...api.getArrowProps()}>
<div {...api.getArrowTipProps()} />
</div>

<h2 style={{ margin: "0 0 12px" }}>Heavy Popover Content</h2>

{/* Lots of nested DOM nodes to make style recalc expensive */}
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{items.map((item) => (
<div
key={item.id}
style={{
padding: 10,
border: "1px solid #eee",
borderRadius: 4,
display: "flex",
flexDirection: "column",
gap: 4,
}}
>
<strong>{item.title}</strong>
<span style={{ fontSize: 13, color: "#666" }}>{item.description}</span>
<div style={{ display: "flex", gap: 4 }}>
<span
style={{
fontSize: 11,
padding: "2px 6px",
background: "#f0f0f0",
borderRadius: 3,
}}
>
tag-a
</span>
<span
style={{
fontSize: 11,
padding: "2px 6px",
background: "#f0f0f0",
borderRadius: 3,
}}
>
tag-b
</span>
</div>
</div>
))}
</div>

<button
{...api.getCloseTriggerProps()}
style={{
marginTop: 12,
padding: "6px 12px",
position: "sticky",
bottom: 0,
background: "white",
}}
>
Close
</button>
</Presence>
</div>
</Portal>
</div>

{/* Tall content to enable page scrolling */}
<div style={{ marginTop: 40, display: "flex", flexDirection: "column", gap: 24 }}>
{pageContent.map((section, i) => (
<div key={i} style={{ padding: 24, background: "#f8f8f8", borderRadius: 8 }}>
<h3>{section}</h3>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
</p>
</div>
))}
</div>
</main>
)
}
1 change: 0 additions & 1 deletion examples/nuxt-ts/app/pages/date-picker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ const api = computed(() => datePicker.connect(service, normalizeProps))
</output>

<div v-bind="api.getControlProps()">
<input v-bind="api.getInputProps()" />
<input v-bind="api.getInputProps()" />
<button v-bind="api.getClearTriggerProps()">❌</button>
<button v-bind="api.getTriggerProps()">🗓</button>
Expand Down
100 changes: 100 additions & 0 deletions examples/nuxt-ts/app/pages/popover-perf.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<script setup lang="ts">
import * as popover from "@zag-js/popover"
import { normalizeProps, useMachine } from "@zag-js/vue"

const service = useMachine(popover.machine, {
id: useId(),
positioning: { placement: "right-start", sizeMiddleware: false },
})

const api = computed(() => popover.connect(service, normalizeProps))

const items = Array.from({ length: 200 }, (_, i) => ({
id: i,
title: `Item ${i + 1}`,
description: `This is a detailed description for item ${i + 1} that adds more DOM nodes and content weight.`,
}))

const sections = Array.from({ length: 50 }, (_, i) => `Section ${i + 1}`)
</script>

<template>
<main style="padding: 40px">
<h1>Popover Performance Test</h1>
<p style="margin-bottom: 16px; color: #666">
Open the popover, then scroll the page. Watch for sluggish position updates in DevTools Performance tab.
</p>

<div style="position: sticky; top: 0; background: white; padding: 12px 0; border-bottom: 1px solid #eee">
<button v-bind="api.getTriggerProps()" style="padding: 8px 16px; font-size: 16px">Open heavy popover</button>

<Teleport to="body" :defer="true">
<div v-bind="api.getPositionerProps()">
<Presence
v-bind="api.getContentProps()"
style="
background: white;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
width: 380px;
max-height: 500px;
overflow: auto;
padding: 16px;
z-index: 1000;
"
>
<div v-bind="api.getArrowProps()">
<div v-bind="api.getArrowTipProps()" />
</div>

<h2 style="margin: 0 0 12px">Heavy Popover Content</h2>

<div style="display: flex; flex-direction: column; gap: 8px">
<div
v-for="item in items"
:key="item.id"
style="
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
display: flex;
flex-direction: column;
gap: 4px;
"
>
<strong>{{ item.title }}</strong>
<span style="font-size: 13px; color: #666">{{ item.description }}</span>
<div style="display: flex; gap: 4px">
<span style="font-size: 11px; padding: 2px 6px; background: #f0f0f0; border-radius: 3px">
tag-a
</span>
<span style="font-size: 11px; padding: 2px 6px; background: #f0f0f0; border-radius: 3px">
tag-b
</span>
</div>
</div>
</div>

<button
v-bind="api.getCloseTriggerProps()"
style="margin-top: 12px; padding: 6px 12px; position: sticky; bottom: 0; background: white"
>
Close
</button>
</Presence>
</div>
</Teleport>
</div>

<div style="margin-top: 40px; display: flex; flex-direction: column; gap: 24px">
<div v-for="(section, i) in sections" :key="i" style="padding: 24px; background: #f8f8f8; border-radius: 8px">
<h3>{{ section }}</h3>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
</p>
</div>
</div>
</main>
</template>
127 changes: 127 additions & 0 deletions examples/solid-ts/src/routes/popover-perf.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import * as popover from "@zag-js/popover"
import { normalizeProps, useMachine } from "@zag-js/solid"
import { createMemo, createUniqueId, For } from "solid-js"
import { Portal } from "solid-js/web"
import { Presence } from "~/components/presence"

const items = Array.from({ length: 200 }, (_, i) => ({
id: i,
title: `Item ${i + 1}`,
description: `This is a detailed description for item ${i + 1} that adds more DOM nodes and content weight.`,
}))

const sections = Array.from({ length: 50 }, (_, i) => `Section ${i + 1}`)

export default function Page() {
const service = useMachine(popover.machine, {
id: createUniqueId(),
positioning: { placement: "right-start", sizeMiddleware: false },
})

const api = createMemo(() => popover.connect(service, normalizeProps))

return (
<main style={{ padding: "40px" }}>
<h1>Popover Performance Test</h1>
<p style={{ "margin-bottom": "16px", color: "#666" }}>
Open the popover, then scroll the page. Watch for sluggish position updates in DevTools Performance tab.
</p>

<div
style={{
position: "sticky",
top: "0",
background: "white",
padding: "12px 0",
"z-index": "10",
"border-bottom": "1px solid #eee",
}}
>
<button {...api().getTriggerProps()} style={{ padding: "8px 16px", "font-size": "16px" }}>
Open heavy popover
</button>

<Portal>
<div {...api().getPositionerProps()}>
<Presence {...api().getContentProps()} style={{ "max-height": "500px", "overflow-block": "auto" }}>
<div {...api().getArrowProps()}>
<div {...api().getArrowTipProps()} />
</div>

<h2 style={{ margin: "0 0 12px" }}>Heavy Popover Content</h2>

<div style={{ display: "flex", "flex-direction": "column", gap: "8px" }}>
<For each={items}>
{(item) => (
<div
style={{
padding: "10px",
border: "1px solid #eee",
"border-radius": "4px",
display: "flex",
"flex-direction": "column",
gap: "4px",
}}
>
<strong>{item.title}</strong>
<span style={{ "font-size": "13px", color: "#666" }}>{item.description}</span>
<div style={{ display: "flex", gap: "4px" }}>
<span
style={{
"font-size": "11px",
padding: "2px 6px",
background: "#f0f0f0",
"border-radius": "3px",
}}
>
tag-a
</span>
<span
style={{
"font-size": "11px",
padding: "2px 6px",
background: "#f0f0f0",
"border-radius": "3px",
}}
>
tag-b
</span>
</div>
</div>
)}
</For>
</div>

<button
{...api().getCloseTriggerProps()}
style={{
"margin-top": "12px",
padding: "6px 12px",
position: "sticky",
bottom: "0",
background: "white",
}}
>
Close
</button>
</Presence>
</div>
</Portal>
</div>

<div style={{ "margin-top": "40px", display: "flex", "flex-direction": "column", gap: "24px" }}>
<For each={sections}>
{(section) => (
<div style={{ padding: "24px", background: "#f8f8f8", "border-radius": "8px" }}>
<h3>{section}</h3>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
</p>
</div>
)}
</For>
</div>
</main>
)
}
Loading
Loading