فهرست منبع

perf(ui): defer tool status width measurement (#26282)

Luke Parker 4 ماه پیش
والد
کامیت
6f165e23de
1فایلهای تغییر یافته به همراه61 افزوده شده و 63 حذف شده
  1. 61 63
      packages/ui/src/components/tool-status-title.tsx

+ 61 - 63
packages/ui/src/components/tool-status-title.tsx

@@ -1,4 +1,4 @@
-import { Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
+import { Show, createEffect, createMemo, on, onCleanup } from "solid-js"
 import { createStore } from "solid-js/store"
 import { TextShimmer } from "./text-shimmer"
 
@@ -15,10 +15,8 @@ function common(active: string, done: string) {
 }
 
 function contentWidth(el: HTMLSpanElement | undefined) {
-  if (!el) return 0
-  const range = document.createRange()
-  range.selectNodeContents(el)
-  return Math.ceil(range.getBoundingClientRect().width)
+  if (!el) return
+  return `${Math.ceil(el.getBoundingClientRect().width)}px`
 }
 
 export function ToolStatusTitle(props: {
@@ -37,99 +35,99 @@ export function ToolStatusTitle(props: {
   const doneTail = createMemo(() => (suffix() ? split().done : props.doneText))
 
   const [state, setState] = createStore({
-    width: "auto",
-    ready: false,
+    active: props.active,
+    animating: false,
+    width: undefined as string | undefined,
   })
   const width = () => state.width
-  const ready = () => state.ready
+  const active = () => state.active
+  const animating = () => state.animating
   let activeRef: HTMLSpanElement | undefined
   let doneRef: HTMLSpanElement | undefined
+  let widthRef: HTMLSpanElement | undefined
   let frame: number | undefined
-  let readyFrame: number | undefined
+  let finishTimer: ReturnType<typeof setTimeout> | undefined
 
-  const measure = () => {
-    const target = props.active ? activeRef : doneRef
-    const px = contentWidth(target)
-    if (px > 0) setState("width", `${px}px`)
+  const finish = () => {
+    if (frame !== undefined) cancelAnimationFrame(frame)
+    if (finishTimer !== undefined) clearTimeout(finishTimer)
+    frame = undefined
+    finishTimer = undefined
+    setState("animating", false)
+    setState("width", undefined)
   }
 
-  const schedule = () => {
-    if (typeof requestAnimationFrame !== "function") {
-      measure()
+  const animate = () => {
+    const first = contentWidth(widthRef)
+    finish()
+    setState("animating", true)
+    setState("active", props.active)
+    const last = contentWidth(props.active ? activeRef : doneRef)
+    if (!first || !last) {
+      finish()
       return
     }
-    if (frame !== undefined) cancelAnimationFrame(frame)
-    frame = requestAnimationFrame(() => {
-      frame = undefined
-      measure()
-    })
-  }
 
-  const finish = () => {
-    if (typeof requestAnimationFrame !== "function") {
-      setState("ready", true)
+    setState("width", first)
+    if (first === last) {
+      finishTimer = setTimeout(finish, 600)
       return
     }
-    if (readyFrame !== undefined) cancelAnimationFrame(readyFrame)
-    readyFrame = requestAnimationFrame(() => {
-      readyFrame = undefined
-      setState("ready", true)
+
+    frame = requestAnimationFrame(() => {
+      frame = undefined
+      setState("width", last)
+      finishTimer = setTimeout(finish, 600)
     })
   }
 
-  createEffect(on([() => props.active, activeTail, doneTail, suffix], () => schedule()))
-
-  onMount(() => {
-    measure()
-    const fonts = typeof document !== "undefined" ? document.fonts : undefined
-    if (!fonts) {
-      finish()
-      return
-    }
-    void fonts.ready.finally(() => {
-      measure()
-      finish()
-    })
-  })
+  createEffect(on([() => props.active, activeTail, doneTail], () => animate(), { defer: true }))
 
   onCleanup(() => {
-    if (frame !== undefined) cancelAnimationFrame(frame)
-    if (readyFrame !== undefined) cancelAnimationFrame(readyFrame)
+    finish()
   })
 
   return (
     <span
       data-component="tool-status-title"
-      data-active={props.active ? "true" : "false"}
-      data-ready={ready() ? "true" : "false"}
+      data-active={active() ? "true" : "false"}
+      data-ready={animating() ? "true" : "false"}
       data-mode={suffix() ? "suffix" : "swap"}
       class={props.class}
-      aria-label={props.active ? props.activeText : props.doneText}
+      aria-label={active() ? props.activeText : props.doneText}
     >
       <Show
         when={suffix()}
         fallback={
-          <span data-slot="tool-status-swap" style={{ width: width() }}>
-            <span data-slot="tool-status-active" ref={activeRef}>
-              <TextShimmer text={activeTail()} active={props.active} offset={0} />
-            </span>
-            <span data-slot="tool-status-done" ref={doneRef}>
-              <TextShimmer text={doneTail()} active={false} offset={0} />
-            </span>
+          <span data-slot="tool-status-swap" ref={widthRef} style={{ width: width() }}>
+            <Show when={animating() || active()}>
+              <span data-slot="tool-status-active" ref={activeRef}>
+                <TextShimmer text={activeTail()} active={active()} offset={0} />
+              </span>
+            </Show>
+            <Show when={animating() || !active()}>
+              <span data-slot="tool-status-done" ref={doneRef}>
+                <TextShimmer text={doneTail()} active={false} offset={0} />
+              </span>
+            </Show>
           </span>
         }
       >
         <span data-slot="tool-status-suffix">
           <span data-slot="tool-status-prefix">
-            <TextShimmer text={split().prefix} active={props.active} offset={0} />
+            <TextShimmer text={split().prefix} active={active()} offset={0} />
           </span>
-          <span data-slot="tool-status-tail" style={{ width: width() }}>
-            <span data-slot="tool-status-active" ref={activeRef}>
-              <TextShimmer text={activeTail()} active={props.active} offset={prefixLen()} />
-            </span>
-            <span data-slot="tool-status-done" ref={doneRef}>
-              <TextShimmer text={doneTail()} active={false} offset={prefixLen()} />
-            </span>
+          <span data-slot="tool-status-tail" ref={widthRef} style={{ width: width() }}>
+            <Show when={animating() || active()}>
+              <span data-slot="tool-status-active" ref={activeRef}>
+                <TextShimmer text={activeTail()} active={active()} offset={prefixLen()} />
+              </span>
+            </Show>
+            <Show when={animating() || !active()}>
+              <span data-slot="tool-status-done" ref={doneRef}>
+                <TextShimmer text={doneTail()} active={false} offset={prefixLen()} />
+              </span>
+            </Show>
           </span>
         </span>
       </Show>