perf(ui): memoize changes lists and reduce stream rendering
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { For, Show, type Accessor, type Component, type JSX } from "solid-js"
|
import { For, Show, createMemo, type Accessor, type Component, type JSX } from "solid-js"
|
||||||
|
|
||||||
import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer"
|
import { MonacoDiffViewer } from "../../../../file-viewer/monaco-diff-viewer"
|
||||||
|
|
||||||
@@ -32,14 +32,18 @@ interface ChangesTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ChangesTab: Component<ChangesTabProps> = (props) => {
|
const ChangesTab: Component<ChangesTabProps> = (props) => {
|
||||||
const renderContent = (): JSX.Element => {
|
const sessionId = createMemo(() => props.activeSessionId())
|
||||||
const sessionId = props.activeSessionId()
|
const hasSession = createMemo(() => Boolean(sessionId() && sessionId() !== "info"))
|
||||||
|
const diffs = createMemo(() => (hasSession() ? props.activeSessionDiffs() : null))
|
||||||
|
|
||||||
const hasSession = Boolean(sessionId && sessionId !== "info")
|
const sorted = createMemo<any[]>(() => {
|
||||||
const diffs = hasSession ? props.activeSessionDiffs() : null
|
const list = diffs()
|
||||||
|
if (!Array.isArray(list)) return []
|
||||||
|
return [...list].sort((a, b) => String(a.file || "").localeCompare(String(b.file || "")))
|
||||||
|
})
|
||||||
|
|
||||||
const sorted = Array.isArray(diffs) ? [...diffs].sort((a, b) => String(a.file || "").localeCompare(String(b.file || ""))) : []
|
const totals = createMemo(() => {
|
||||||
const totals = sorted.reduce(
|
return sorted().reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
acc.additions += typeof item.additions === "number" ? item.additions : 0
|
acc.additions += typeof item.additions === "number" ? item.additions : 0
|
||||||
acc.deletions += typeof item.deletions === "number" ? item.deletions : 0
|
acc.deletions += typeof item.deletions === "number" ? item.deletions : 0
|
||||||
@@ -47,41 +51,61 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
},
|
},
|
||||||
{ additions: 0, deletions: 0 },
|
{ additions: 0, deletions: 0 },
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const mostChanged = sorted.length
|
const mostChanged = createMemo<any | null>(() => {
|
||||||
? sorted.reduce((best, item) => {
|
const items = sorted()
|
||||||
const bestAdd = typeof (best as any)?.additions === "number" ? (best as any).additions : 0
|
if (items.length === 0) return null
|
||||||
const bestDel = typeof (best as any)?.deletions === "number" ? (best as any).deletions : 0
|
return items.reduce((best, item) => {
|
||||||
const bestScore = bestAdd + bestDel
|
const bestAdd = typeof (best as any)?.additions === "number" ? (best as any).additions : 0
|
||||||
|
const bestDel = typeof (best as any)?.deletions === "number" ? (best as any).deletions : 0
|
||||||
|
const bestScore = bestAdd + bestDel
|
||||||
|
|
||||||
const add = typeof (item as any)?.additions === "number" ? (item as any).additions : 0
|
const add = typeof (item as any)?.additions === "number" ? (item as any).additions : 0
|
||||||
const del = typeof (item as any)?.deletions === "number" ? (item as any).deletions : 0
|
const del = typeof (item as any)?.deletions === "number" ? (item as any).deletions : 0
|
||||||
const score = add + del
|
const score = add + del
|
||||||
|
|
||||||
if (score > bestScore) return item
|
if (score > bestScore) return item
|
||||||
if (score < bestScore) return best
|
if (score < bestScore) return best
|
||||||
return String(item.file || "").localeCompare(String((best as any)?.file || "")) < 0 ? item : best
|
return String(item.file || "").localeCompare(String((best as any)?.file || "")) < 0 ? item : best
|
||||||
}, sorted[0])
|
}, items[0])
|
||||||
: null
|
})
|
||||||
|
|
||||||
// Auto-select the most-changed file if none selected.
|
const selectedFileData = createMemo<any | null>(() => {
|
||||||
const currentSelected = props.selectedFile()
|
const currentSelected = props.selectedFile()
|
||||||
const selectedFileData = sorted.find((f) => f.file === currentSelected) || mostChanged
|
const items = sorted()
|
||||||
|
if (currentSelected) {
|
||||||
const scopeKey = `${props.instanceId}:${hasSession ? sessionId : "no-session"}`
|
const match = items.find((f) => f.file === currentSelected)
|
||||||
|
if (match) return match
|
||||||
const emptyViewerMessage = () => {
|
|
||||||
if (!hasSession) return props.t("instanceShell.sessionChanges.noSessionSelected")
|
|
||||||
if (diffs === undefined) return props.t("instanceShell.sessionChanges.loading")
|
|
||||||
if (!Array.isArray(diffs) || diffs.length === 0) return props.t("instanceShell.sessionChanges.empty")
|
|
||||||
return props.t("instanceShell.filesShell.viewerEmpty")
|
|
||||||
}
|
}
|
||||||
|
return mostChanged()
|
||||||
|
})
|
||||||
|
|
||||||
|
const scopeKey = createMemo(() => `${props.instanceId}:${hasSession() ? sessionId() : "no-session"}`)
|
||||||
|
|
||||||
|
const emptyViewerMessage = createMemo(() => {
|
||||||
|
if (!hasSession()) return props.t("instanceShell.sessionChanges.noSessionSelected")
|
||||||
|
const currentDiffs = diffs()
|
||||||
|
if (currentDiffs === undefined) return props.t("instanceShell.sessionChanges.loading")
|
||||||
|
if (!Array.isArray(currentDiffs) || currentDiffs.length === 0) return props.t("instanceShell.sessionChanges.empty")
|
||||||
|
return props.t("instanceShell.filesShell.viewerEmpty")
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerPath = createMemo(() => {
|
||||||
|
const file = selectedFileData()
|
||||||
|
return file?.file ? String(file.file) : props.t("instanceShell.rightPanel.tabs.changes")
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderContent = (): JSX.Element => {
|
||||||
|
const sortedList = sorted()
|
||||||
|
const totalsValue = totals()
|
||||||
|
const selected = selectedFileData()
|
||||||
|
|
||||||
const renderViewer = () => (
|
const renderViewer = () => (
|
||||||
<div class="file-viewer-panel flex-1">
|
<div class="file-viewer-panel flex-1">
|
||||||
<div class="file-viewer-content file-viewer-content--monaco">
|
<div class="file-viewer-content file-viewer-content--monaco">
|
||||||
<Show
|
<Show
|
||||||
when={selectedFileData && hasSession && Array.isArray(diffs) && diffs.length > 0 ? selectedFileData : null}
|
when={selected && hasSession() && sortedList.length > 0 ? selected : null}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="file-viewer-empty">
|
<div class="file-viewer-empty">
|
||||||
<span class="file-viewer-empty-text">{emptyViewerMessage()}</span>
|
<span class="file-viewer-empty-text">{emptyViewerMessage()}</span>
|
||||||
@@ -90,7 +114,7 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
>
|
>
|
||||||
{(file) => (
|
{(file) => (
|
||||||
<MonacoDiffViewer
|
<MonacoDiffViewer
|
||||||
scopeKey={scopeKey}
|
scopeKey={scopeKey()}
|
||||||
path={String(file().file || "")}
|
path={String(file().file || "")}
|
||||||
before={String((file() as any).before || "")}
|
before={String((file() as any).before || "")}
|
||||||
after={String((file() as any).after || "")}
|
after={String((file() as any).after || "")}
|
||||||
@@ -109,11 +133,11 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const renderListPanel = () => (
|
const renderListPanel = () => (
|
||||||
<Show when={sorted.length > 0} fallback={renderEmptyList()}>
|
<Show when={sortedList.length > 0} fallback={renderEmptyList()}>
|
||||||
<For each={sorted}>
|
<For each={sortedList}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div
|
<div
|
||||||
class={`file-list-item ${selectedFileData?.file === item.file ? "file-list-item-active" : ""}`}
|
class={`file-list-item ${selected?.file === item.file ? "file-list-item-active" : ""}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.onSelectFile(item.file, props.isPhoneLayout())
|
props.onSelectFile(item.file, props.isPhoneLayout())
|
||||||
}}
|
}}
|
||||||
@@ -134,11 +158,11 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const renderListOverlay = () => (
|
const renderListOverlay = () => (
|
||||||
<Show when={sorted.length > 0} fallback={renderEmptyList()}>
|
<Show when={sortedList.length > 0} fallback={renderEmptyList()}>
|
||||||
<For each={sorted}>
|
<For each={sortedList}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div
|
<div
|
||||||
class={`file-list-item ${selectedFileData?.file === item.file ? "file-list-item-active" : ""}`}
|
class={`file-list-item ${selected?.file === item.file ? "file-list-item-active" : ""}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.onSelectFile(item.file, true)
|
props.onSelectFile(item.file, true)
|
||||||
}}
|
}}
|
||||||
@@ -159,8 +183,6 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|
||||||
const headerPath = () => (selectedFileData?.file ? selectedFileData.file : props.t("instanceShell.rightPanel.tabs.changes"))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SplitFilePanel
|
<SplitFilePanel
|
||||||
header={
|
header={
|
||||||
@@ -171,10 +193,10 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
|
|||||||
|
|
||||||
<div class="files-tab-stats" style={{ flex: "0 0 auto" }}>
|
<div class="files-tab-stats" style={{ flex: "0 0 auto" }}>
|
||||||
<span class="files-tab-stat files-tab-stat-additions">
|
<span class="files-tab-stat files-tab-stat-additions">
|
||||||
<span class="files-tab-stat-value">+{totals.additions}</span>
|
<span class="files-tab-stat-value">+{totalsValue.additions}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="files-tab-stat files-tab-stat-deletions">
|
<span class="files-tab-stat files-tab-stat-deletions">
|
||||||
<span class="files-tab-stat-value">-{totals.deletions}</span>
|
<span class="files-tab-stat-value">-{totalsValue.deletions}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { For, Show, type Accessor, type Component, type JSX } from "solid-js"
|
import { For, Show, createMemo, type Accessor, type Component, type JSX } from "solid-js"
|
||||||
import type { File as GitFileStatus } from "@opencode-ai/sdk/v2/client"
|
import type { File as GitFileStatus } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
import { RefreshCw } from "lucide-solid"
|
import { RefreshCw } from "lucide-solid"
|
||||||
@@ -46,17 +46,18 @@ interface GitChangesTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
||||||
const renderContent = (): JSX.Element => {
|
const sessionId = createMemo(() => props.activeSessionId())
|
||||||
const sessionId = props.activeSessionId()
|
const hasSession = createMemo(() => Boolean(sessionId() && sessionId() !== "info"))
|
||||||
|
const entries = createMemo(() => (hasSession() ? props.entries() : null))
|
||||||
|
|
||||||
const hasSession = Boolean(sessionId && sessionId !== "info")
|
const sorted = createMemo<GitFileStatus[]>(() => {
|
||||||
const entries = hasSession ? props.entries() : null
|
const list = entries()
|
||||||
|
if (!Array.isArray(list)) return []
|
||||||
|
return [...list].sort((a, b) => String(a.path || "").localeCompare(String(b.path || "")))
|
||||||
|
})
|
||||||
|
|
||||||
const sorted = Array.isArray(entries)
|
const totals = createMemo(() => {
|
||||||
? [...entries].sort((a, b) => String(a.path || "").localeCompare(String(b.path || "")))
|
return sorted().reduce(
|
||||||
: []
|
|
||||||
|
|
||||||
const totals = sorted.reduce(
|
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
acc.additions += typeof item.added === "number" ? item.added : 0
|
acc.additions += typeof item.added === "number" ? item.added : 0
|
||||||
acc.deletions += typeof item.removed === "number" ? item.removed : 0
|
acc.deletions += typeof item.removed === "number" ? item.removed : 0
|
||||||
@@ -64,21 +65,33 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
},
|
},
|
||||||
{ additions: 0, deletions: 0 },
|
{ additions: 0, deletions: 0 },
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const nonDeleted = sorted.filter((item) => item && item.status !== "deleted")
|
const nonDeleted = createMemo(() => sorted().filter((item) => item && item.status !== "deleted"))
|
||||||
|
|
||||||
const emptyViewerMessage = () => {
|
|
||||||
if (!hasSession) return "Select a session to view changes."
|
|
||||||
if (entries === null) return "Loading git changes…"
|
|
||||||
if (nonDeleted.length === 0) return "No git changes yet."
|
|
||||||
return "No file selected."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const selectedEntry = createMemo<GitFileStatus | null>(() => {
|
||||||
|
const list = sorted()
|
||||||
const selectedPath = props.selectedPath()
|
const selectedPath = props.selectedPath()
|
||||||
const fallbackPath = props.mostChangedPath()
|
const fallbackPath = props.mostChangedPath()
|
||||||
const selectedEntry =
|
const found =
|
||||||
sorted.find((item) => item.path === selectedPath) ||
|
list.find((item) => item.path === selectedPath) ||
|
||||||
(fallbackPath ? sorted.find((item) => item.path === fallbackPath) : null)
|
(fallbackPath ? list.find((item) => item.path === fallbackPath) : undefined)
|
||||||
|
return found ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const emptyViewerMessage = createMemo(() => {
|
||||||
|
if (!hasSession()) return "Select a session to view changes."
|
||||||
|
const currentEntries = entries()
|
||||||
|
if (currentEntries === null) return "Loading git changes…"
|
||||||
|
if (nonDeleted().length === 0) return "No git changes yet."
|
||||||
|
return "No file selected."
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderContent = (): JSX.Element => {
|
||||||
|
const totalsValue = totals()
|
||||||
|
const selected = selectedEntry()
|
||||||
|
const sortedList = sorted()
|
||||||
|
const nonDeletedList = nonDeleted()
|
||||||
|
|
||||||
const renderViewer = () => (
|
const renderViewer = () => (
|
||||||
<div class="file-viewer-panel flex-1">
|
<div class="file-viewer-panel flex-1">
|
||||||
@@ -91,12 +104,12 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
fallback={
|
fallback={
|
||||||
<Show
|
<Show
|
||||||
when={
|
when={
|
||||||
selectedEntry &&
|
selected &&
|
||||||
props.selectedBefore() !== null &&
|
props.selectedBefore() !== null &&
|
||||||
props.selectedAfter() !== null &&
|
props.selectedAfter() !== null &&
|
||||||
selectedEntry.status !== "deleted"
|
selected.status !== "deleted"
|
||||||
? {
|
? {
|
||||||
path: selectedEntry.path,
|
path: selected.path,
|
||||||
before: props.selectedBefore() as string,
|
before: props.selectedBefore() as string,
|
||||||
after: props.selectedAfter() as string,
|
after: props.selectedAfter() as string,
|
||||||
}
|
}
|
||||||
@@ -109,16 +122,16 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(file) => (
|
{(file) => (
|
||||||
<MonacoDiffViewer
|
<MonacoDiffViewer
|
||||||
scopeKey={props.scopeKey()}
|
scopeKey={props.scopeKey()}
|
||||||
path={String(file().path || "")}
|
path={String(file().path || "")}
|
||||||
before={String((file() as any).before || "")}
|
before={String((file() as any).before || "")}
|
||||||
after={String((file() as any).after || "")}
|
after={String((file() as any).after || "")}
|
||||||
viewMode={props.diffViewMode()}
|
viewMode={props.diffViewMode()}
|
||||||
contextMode={props.diffContextMode()}
|
contextMode={props.diffContextMode()}
|
||||||
wordWrap={props.diffWordWrapMode()}
|
wordWrap={props.diffWordWrapMode()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -141,8 +154,8 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
const renderEmptyList = () => <div class="p-3 text-xs text-secondary">{emptyViewerMessage()}</div>
|
const renderEmptyList = () => <div class="p-3 text-xs text-secondary">{emptyViewerMessage()}</div>
|
||||||
|
|
||||||
const renderListPanel = () => (
|
const renderListPanel = () => (
|
||||||
<Show when={nonDeleted.length > 0} fallback={renderEmptyList()}>
|
<Show when={nonDeletedList.length > 0} fallback={renderEmptyList()}>
|
||||||
<For each={sorted}>
|
<For each={sortedList}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div
|
<div
|
||||||
class={`file-list-item ${props.selectedPath() === item.path ? "file-list-item-active" : ""}`}
|
class={`file-list-item ${props.selectedPath() === item.path ? "file-list-item-active" : ""}`}
|
||||||
@@ -173,8 +186,8 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const renderListOverlay = () => (
|
const renderListOverlay = () => (
|
||||||
<Show when={nonDeleted.length > 0} fallback={renderEmptyList()}>
|
<Show when={nonDeletedList.length > 0} fallback={renderEmptyList()}>
|
||||||
<For each={sorted}>
|
<For each={sortedList}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div
|
<div
|
||||||
class={`file-list-item ${props.selectedPath() === item.path ? "file-list-item-active" : ""}`}
|
class={`file-list-item ${props.selectedPath() === item.path ? "file-list-item-active" : ""}`}
|
||||||
@@ -204,19 +217,19 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SplitFilePanel
|
<SplitFilePanel
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
<span class="files-tab-selected-path" title={selectedEntry?.path || "Git Changes"}>
|
<span class="files-tab-selected-path" title={selected?.path || "Git Changes"}>
|
||||||
<span class="file-path-text">{selectedEntry?.path || "Git Changes"}</span>
|
<span class="file-path-text">{selected?.path || "Git Changes"}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="files-tab-stats" style={{ flex: "0 0 auto" }}>
|
<div class="files-tab-stats" style={{ flex: "0 0 auto" }}>
|
||||||
<span class="files-tab-stat files-tab-stat-additions">
|
<span class="files-tab-stat files-tab-stat-additions">
|
||||||
<span class="files-tab-stat-value">+{totals.additions}</span>
|
<span class="files-tab-stat-value">+{totalsValue.additions}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="files-tab-stat files-tab-stat-deletions">
|
<span class="files-tab-stat files-tab-stat-deletions">
|
||||||
<span class="files-tab-stat-value">-{totals.deletions}</span>
|
<span class="files-tab-stat-value">-{totalsValue.deletions}</span>
|
||||||
</span>
|
</span>
|
||||||
<Show when={props.statusError()}>{(err) => <span class="text-error">{err()}</span>}</Show>
|
<Show when={props.statusError()}>{(err) => <span class="text-error">{err()}</span>}</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -226,23 +239,23 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
|
|||||||
class="files-header-icon-button"
|
class="files-header-icon-button"
|
||||||
title={props.t("instanceShell.rightPanel.actions.refresh")}
|
title={props.t("instanceShell.rightPanel.actions.refresh")}
|
||||||
aria-label={props.t("instanceShell.rightPanel.actions.refresh")}
|
aria-label={props.t("instanceShell.rightPanel.actions.refresh")}
|
||||||
disabled={!hasSession || props.statusLoading() || entries === null}
|
disabled={!hasSession() || props.statusLoading() || entries() === null}
|
||||||
style={{ "margin-left": "auto" }}
|
style={{ "margin-left": "auto" }}
|
||||||
onClick={() => props.onRefresh()}
|
onClick={() => props.onRefresh()}
|
||||||
>
|
>
|
||||||
<RefreshCw class={`h-4 w-4${props.statusLoading() ? " animate-spin" : ""}`} />
|
<RefreshCw class={`h-4 w-4${props.statusLoading() ? " animate-spin" : ""}`} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<DiffToolbar
|
<DiffToolbar
|
||||||
viewMode={props.diffViewMode()}
|
viewMode={props.diffViewMode()}
|
||||||
contextMode={props.diffContextMode()}
|
contextMode={props.diffContextMode()}
|
||||||
wordWrapMode={props.diffWordWrapMode()}
|
wordWrapMode={props.diffWordWrapMode()}
|
||||||
onViewModeChange={props.onViewModeChange}
|
onViewModeChange={props.onViewModeChange}
|
||||||
onContextModeChange={props.onContextModeChange}
|
onContextModeChange={props.onContextModeChange}
|
||||||
onWordWrapModeChange={props.onWordWrapModeChange}
|
onWordWrapModeChange={props.onWordWrapModeChange}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
list={{ panel: renderListPanel, overlay: renderListOverlay }}
|
list={{ panel: renderListPanel, overlay: renderListOverlay }}
|
||||||
viewer={renderViewer()}
|
viewer={renderViewer()}
|
||||||
listOpen={props.listOpen()}
|
listOpen={props.listOpen()}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export function getMessageAnchorId(messageId: string) {
|
|||||||
return `message-anchor-${messageId}`
|
return `message-anchor-${messageId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIRTUAL_ITEM_MARGIN_PX = 800
|
const VIRTUAL_ITEM_MARGIN_PX = 300
|
||||||
|
|
||||||
interface MessageBlockListProps {
|
interface MessageBlockListProps {
|
||||||
instanceId: string
|
instanceId: string
|
||||||
|
|||||||
@@ -8,6 +8,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.0625rem;
|
gap: 0.0625rem;
|
||||||
|
|
||||||
|
/* Reduce render + paint work for offscreen (but mounted) blocks.
|
||||||
|
Keep a conservative intrinsic size to avoid scroll jumps. */
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 1px 400px;
|
||||||
|
contain: layout paint style;
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtual-item-wrapper {
|
.virtual-item-wrapper {
|
||||||
|
|||||||
Reference in New Issue
Block a user