Optimize tippy by lazily instantiation

This commit is contained in:
Jon Staab 2026-08-29 10:12:09 -07:00
parent ee09c975f8
commit 749e87f228
14 changed files with 66 additions and 29 deletions

View file

@ -56,11 +56,12 @@
if (popoverIsVisible) {
popover?.hide()
} else {
popover?.show()
showPopover()
}
}
let popover: Instance | undefined = $state()
let showPopover: () => void = $state(() => {})
let popoverIsVisible = $state(false)
</script>
@ -71,6 +72,7 @@
{#if !isMobile}
<Tippy
bind:popover
bind:show={showPopover}
component={ChatMessageMenu}
props={{event, pubkeys, popover, replyTo, edit}}
params={{

View file

@ -55,11 +55,12 @@
let wrapper: Element | undefined = $state()
let popover: Instance | undefined = $state()
let showPopover: () => void = $state(() => {})
let instance: any = $state()
$effect(() => {
if ($term) {
popover?.show()
showPopover()
} else {
popover?.hide()
}
@ -82,6 +83,7 @@
{/if}
<Tippy
bind:popover
bind:show={showPopover}
bind:instance
component={Suggestions}
props={{

View file

@ -28,8 +28,6 @@
const shouldProtect = $relays.hasNip(url, 70)
const showPopover = () => popover?.show()
const hidePopover = () => popover?.hide()
const onEmoji = async (emoji: NativeEmoji) => {
@ -48,6 +46,7 @@
}
let popover: Instance | undefined = $state()
let showPopover: () => void = $state(() => {})
</script>
<div class="items-center join">
@ -62,6 +61,7 @@
<Button onclick={showPopover} class="flex join-item button button-neutral button-xs">
<Tippy
bind:popover
bind:show={showPopover}
component={EventMenu}
props={{url, noun, event, customActions, onClick: hidePopover}}
params={{trigger: "manual", interactive: true}}>

View file

@ -15,16 +15,16 @@
const {pubkey, url, customActions}: Props = $props()
const showPopover = () => popover?.show()
const hidePopover = () => popover?.hide()
let popover: Instance | undefined = $state()
let showPopover: () => void = $state(() => {})
</script>
<Button onclick={showPopover} class="button button-circle button-ghost button-sm">
<Tippy
bind:popover
bind:show={showPopover}
component={ProfileMenuList}
props={{pubkey, url, customActions, onClick: hidePopover}}
params={{trigger: "manual", interactive: true, placement: "bottom-end"}}>

View file

@ -55,6 +55,7 @@
let label: Element | undefined = $state()
let popover: Instance | undefined = $state()
let showPopover: () => void = $state(() => {})
let instance: any = $state()
$effect(() => {
@ -62,7 +63,7 @@
oninput?.($term)
if ($term) {
popover?.show()
showPopover()
} else {
popover?.hide()
}
@ -97,6 +98,7 @@
</label>
<Tippy
bind:popover
bind:show={showPopover}
bind:instance
component={Suggestions}
props={{

View file

@ -52,8 +52,6 @@
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
const showPopover = () => popover?.show()
const hidePopover = () => popover?.hide()
const submit = async () => {
@ -70,6 +68,7 @@
}
let popover: Instance | undefined = $state()
let showPopover: () => void = $state(() => {})
let content = $state(
initialValues?.type === "text" ? initialValues.value : (draftKey?.get()?.content ?? ""),
)
@ -126,6 +125,7 @@
class="join-item h-10 w-10 min-w-10 button button-neutral">
<Tippy
bind:popover
bind:show={showPopover}
component={ComposeMenu}
props={{url, h, onClick: hidePopover}}
params={{trigger: "manual", interactive: true}}>

View file

@ -9,8 +9,6 @@
const {url, event} = $props()
const open = () => popover?.show()
const onClick = () => popover?.hide()
const onShow = () => {
@ -32,6 +30,7 @@
}
let popover: Instance | undefined = $state()
let open: () => void = $state(() => {})
let visible = $state(false)
</script>
@ -40,6 +39,7 @@
<Button onclick={open} class="button button-xs button-neutral join-item">
<Tippy
bind:popover
bind:show={open}
component={RoomItemMenu}
props={{url, event, onClick}}
params={{trigger: "manual", interactive: true, onShow, onHidden}}>

View file

@ -67,11 +67,12 @@
let label: Element | undefined = $state()
let popover: Instance | undefined = $state()
let showPopover: () => void = $state(() => {})
let instance: any = $state()
$effect(() => {
if ($term.trim()) {
popover?.show()
showPopover()
} else {
popover?.hide()
}
@ -101,6 +102,7 @@
</label>
<Tippy
bind:popover
bind:show={showPopover}
bind:instance
component={Suggestions}
props={{

View file

@ -8,8 +8,6 @@
const {tippyParams = {}, ...props} = $props()
const open = () => popover?.show()
const onClick = (emoji: NativeEmoji) => {
props.onEmoji(emoji)
popover?.hide()
@ -34,6 +32,7 @@
})
let popover: Instance | undefined = $state()
let open: () => void = $state(() => {})
let visible = $state(false)
</script>
@ -42,6 +41,7 @@
<Button onclick={open} class={props.class}>
<Tippy
bind:popover
bind:show={open}
class="flex"
component={EmojiPicker}
props={{onClick}}

View file

@ -14,7 +14,7 @@
if (isMobile) {
pushModal(IconPickerModal, {onSelect: onClick}, {nested: true})
} else {
popover?.show()
showPopover()
}
}
@ -42,12 +42,14 @@
})
let popover: Instance | undefined = $state()
let showPopover: () => void = $state(() => {})
</script>
<svelte:document onmousemove={onMouseMove} />
<Tippy
bind:popover
bind:show={showPopover}
component={IconPickerPopover}
props={{onSelect: onClick}}
params={{trigger: "manual", interactive: true, placement: "top-end"}}>

View file

@ -22,16 +22,16 @@
"aria-label": ariaLabel,
}: Props = $props()
const showPopover = () => popover?.show()
const hidePopover = () => popover?.hide()
let popover: Instance | undefined = $state()
let showPopover: () => void = $state(() => {})
</script>
<Button class={className} aria-label={ariaLabel} onclick={showPopover}>
<Tippy
bind:popover
bind:show={showPopover}
{component}
props={{...componentProps, onClick: hidePopover}}
params={{trigger: "manual", interactive: true, placement: "bottom-end"}}>

View file

@ -4,7 +4,7 @@
import tippy from "tippy.js"
import type {Instance} from "tippy.js"
import {onMount, mount, unmount} from "svelte"
import {isMobile} from "@lib/html"
import {getTippyTarget, isMobile} from "@lib/html"
let {
component,
@ -13,6 +13,7 @@
params = {},
popover = $bindable(),
instance = $bindable(),
show = $bindable(),
...restProps
} = $props()
@ -23,17 +24,14 @@
// the props it was first mounted with, showing stale data after the source updates.
const mountedProps = $state({...props})
$effect(() => {
Object.assign(mountedProps, props)
})
onMount(() => {
const target = document.createElement("div")
popover = tippy(element, {
// Building a tippy costs a popper element and a set of listeners, which is wasted on the
// hover menus of a chat row nobody ever opens. Only a real trigger needs the instance up
// front — tippy is the one listening for it. A manual one can wait for `show`.
const create = () => {
popover ??= tippy(element, {
content: target,
animation: "shift-away",
appendTo: document.querySelector(".tippy-target")!,
appendTo: getTippyTarget(),
trigger: isMobile ? "click" : "mouseenter focus",
...params,
onShow: (tippyInstance: Instance) => {
@ -43,6 +41,22 @@
},
})
return popover
}
const target = document.createElement("div")
show = () => create().show()
$effect(() => {
Object.assign(mountedProps, props)
})
onMount(() => {
if (params.trigger !== "manual") {
create()
}
return () => {
popover?.destroy()

View file

@ -30,7 +30,7 @@
import tippy from "tippy.js"
import {onMount} from "svelte"
import {isMobile} from "@lib/html"
import {getTippyTarget, isMobile} from "@lib/html"
let {
content,
@ -50,7 +50,7 @@
interactive,
animation: "shift-away",
theme: "tooltip",
appendTo: document.querySelector(".tippy-target")!,
appendTo: getTippyTarget(),
trigger: isMobile ? "click" : "mouseenter focus",
})

View file

@ -1,5 +1,6 @@
import {readable} from "svelte/store"
import {sleep, randomId} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import {Capacitor} from "@capacitor/core"
export {preventDefault, stopPropagation} from "svelte/legacy"
@ -152,6 +153,18 @@ export const createScroller = ({
export const isMobile = "ontouchstart" in document.documentElement
// The layout's single popover host. Cached, because with a couple of popovers per chat row
// this runs thousands of times against a document that is itself thousands of nodes.
let tippyTarget: Maybe<Element>
export const getTippyTarget = () => {
if (!tippyTarget?.isConnected) {
tippyTarget = document.querySelector(".tippy-target")!
}
return tippyTarget
}
export const downloadText = async (filename: string, text: string) => {
// The <a download> blob trick is a no-op in native WebViews (Android in
// particular never triggers a download), so on device we write the file and