Compare commits

..

No commits in common. "main" and "v1.0.0-beta.1" have entirely different histories.

6 changed files with 33 additions and 68 deletions

View File

@ -1,9 +1,5 @@
# CHANGELOG # CHANGELOG
## v1.0.1 (2026-05-14)
- **Perf**: 优化 `FastList` 渲染逻辑,消除 Layout Thrashing提升复杂列表GroupedList/Tree滚动性能达 10 倍以上。
- **Refactor**: 采用 `requestAnimationFrame` 优化初始化高度计算,减少闪烁。
## v1.0.0 (2026-05-14) ## v1.0.0 (2026-05-14)
- **Feat**: 初始化项目结构,建立 ESM 开发环境。 - **Feat**: 初始化项目结构,建立 ESM 开发环境。
- **Refactor**: 将 `base.js` 迁移至 `src/index.js`,采用 ESM 导出。 - **Refactor**: 将 `base.js` 迁移至 `src/index.js`,采用 ESM 导出。

12
TEST.md
View File

@ -3,12 +3,12 @@
## 基准测试 (Benchmark) ## 基准测试 (Benchmark)
*测试环境: Playwright / Chromium* *测试环境: Playwright / Chromium*
| 指标 | v1.0.0 | v1.0.1 | | 指标 | v1.0.0 |
| :--- | :--- | :--- | | :--- | :--- |
| **FastList Render & Scroll (10k items)** | ~535ms | ~473ms | | **FastList Render & Scroll (10k items)** | ~522ms - 1200ms (取决于环境预热) |
| **FastGroupedList Render & Scroll (10k)** | ~705ms | ~51ms | | **FastGroupedList Render & Scroll (10k)** | ~690ms - 820ms |
| **FastTree Render & Scroll (10k items)** | ~927ms | ~50ms | | **FastTree Render & Scroll (10k items)** | ~930ms - 1060ms |
| **CollapseTree Render & Scroll (1.2k)** | ~51ms | ~50ms | | **CollapseTree Render & Scroll (1.2k)** | ~50ms |
## 测试覆盖 (Coverage) ## 测试覆盖 (Coverage)
- [x] HTTP Request (GET/POST) - [x] HTTP Request (GET/POST)

39
dist/base.js vendored
View File

@ -346,26 +346,22 @@ const FastListComponent = Component.register("FastList", (container) => {
let containerPaddingTop = 0; let containerPaddingTop = 0;
let containerRowGap = 0; let containerRowGap = 0;
let topMargin = 0; let topMargin = 0;
let itemMarginTop = null;
let itemMarginBottom = null;
let listStartIndex = 0; let listStartIndex = 0;
let visibleStartIndex = 0; let visibleStartIndex = 0;
let visibleCount = 10; let visibleCount = 10;
let listInited = false; let listInited = false;
container.onItemUpdate = (index2, node) => { container.onItemUpdate = (index2, node) => {
const absoluteIndex = index2 + listStartIndex; const absoluteIndex = index2 + listStartIndex;
if (itemMarginTop === null) {
const style = window.getComputedStyle(node);
itemMarginTop = parseFloat(style.marginTop) || 0;
itemMarginBottom = parseFloat(style.marginBottom) || 0;
}
if (absoluteIndex === 0) topMargin = itemMarginTop;
const newHeight = node.offsetHeight + itemMarginTop + itemMarginBottom + containerRowGap;
const oldHeight = itemHeights.get(absoluteIndex); const oldHeight = itemHeights.get(absoluteIndex);
const style = window.getComputedStyle(node);
const marginTop = parseFloat(style.marginTop) || 0;
const marginBottom = parseFloat(style.marginBottom) || 0;
const newHeight = node.offsetHeight + marginTop + marginBottom + containerRowGap;
if (absoluteIndex === 0 && !topMargin) topMargin = marginTop;
if (newHeight !== oldHeight) { if (newHeight !== oldHeight) {
itemHeights.set(absoluteIndex, newHeight); itemHeights.set(absoluteIndex, newHeight);
avg.add(newHeight); avg.add(newHeight);
const offset = newHeight - (oldHeight || 0); const offset = newHeight - oldHeight || 0;
const groupIndex = absoluteIndex - absoluteIndex % groupItemCount; const groupIndex = absoluteIndex - absoluteIndex % groupItemCount;
if (groupHeights.has(groupIndex)) groupHeights.set(groupIndex, groupHeights.get(groupIndex) + offset); if (groupHeights.has(groupIndex)) groupHeights.set(groupIndex, groupHeights.get(groupIndex) + offset);
} }
@ -415,34 +411,23 @@ const FastListComponent = Component.register("FastList", (container) => {
container.state.renderedList = list; container.state.renderedList = list;
}; };
container.state.__watch("list", (list) => { container.state.__watch("list", (list) => {
console.log("FastList list bound, size:", list == null ? void 0 : list.length, container.id);
listInited = false; listInited = false;
itemHeights.clear(); itemHeights.clear();
groupHeights.clear(); groupHeights.clear();
avg.clear(); avg.clear();
topMargin = 0;
itemMarginTop = null;
itemMarginBottom = null;
if (!(list == null ? void 0 : list.length)) return; if (!(list == null ? void 0 : list.length)) return;
const listSize = list.length || 0; const listSize = list.length || 0;
groupItemCount = Math.ceil(Math.sqrt(listSize)) || 10; groupItemCount = Math.ceil(Math.sqrt(listSize)) || 10;
const style = window.getComputedStyle(container); const style = window.getComputedStyle(container);
containerPaddingTop = parseFloat(style.paddingTop) || 0; containerPaddingTop = parseFloat(style.paddingTop);
containerRowGap = parseFloat(style.rowGap) || 0; containerRowGap = parseFloat(style.rowGap) || 0;
container.state.renderedList = list.slice(0, Math.min(30, listSize)) || []; container.state.renderedList = list.slice(0, Math.min(30, listSize)) || [];
requestAnimationFrame(() => { setTimeout(() => {
if (listInited || container.state.list !== list) return; container.state.list.forEach((item, index2) => itemHeights.set(index2, avg.get()));
const defaultHeight = avg.get() || 32; for (let i = 0; i < listSize; i += groupItemCount) groupHeights.set(i, Math.min(groupItemCount, listSize - i) * avg.get());
for (let i = 0; i < listSize; i++) {
if (!itemHeights.has(i)) itemHeights.set(i, defaultHeight);
}
for (let i = 0; i < listSize; i += groupItemCount) {
let sum = 0;
for (let j = i; j < Math.min(i + groupItemCount, listSize); j++) {
sum += itemHeights.get(j);
}
groupHeights.set(i, sum);
}
listInited = true; listInited = true;
console.log("FastList inited, calling refresh", container.id);
container.refresh(); container.refresh();
}); });
}); });

2
dist/base.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"name": "@web/base", "name": "@web/base",
"version": "1.0.1", "version": "1.0.0",
"type": "module", "type": "module",
"main": "dist/base.js", "main": "dist/base.js",
"module": "dist/base.js", "module": "dist/base.js",

View File

@ -9,8 +9,6 @@ export const FastListComponent = Component.register('FastList', container => {
let containerPaddingTop = 0 let containerPaddingTop = 0
let containerRowGap = 0 let containerRowGap = 0
let topMargin = 0 let topMargin = 0
let itemMarginTop = null
let itemMarginBottom = null
let listStartIndex = 0 let listStartIndex = 0
let visibleStartIndex = 0 let visibleStartIndex = 0
let visibleCount = 10 let visibleCount = 10
@ -18,19 +16,16 @@ export const FastListComponent = Component.register('FastList', container => {
container.onItemUpdate = (index, node) => { container.onItemUpdate = (index, node) => {
const absoluteIndex = index + listStartIndex const absoluteIndex = index + listStartIndex
if (itemMarginTop === null) {
const style = window.getComputedStyle(node)
itemMarginTop = parseFloat(style.marginTop) || 0
itemMarginBottom = parseFloat(style.marginBottom) || 0
}
if (absoluteIndex === 0) topMargin = itemMarginTop
const newHeight = node.offsetHeight + itemMarginTop + itemMarginBottom + containerRowGap
const oldHeight = itemHeights.get(absoluteIndex) const oldHeight = itemHeights.get(absoluteIndex)
const style = window.getComputedStyle(node)
const marginTop = parseFloat(style.marginTop) || 0
const marginBottom = parseFloat(style.marginBottom) || 0
const newHeight = node.offsetHeight + marginTop + marginBottom + containerRowGap
if (absoluteIndex === 0 && !topMargin) topMargin = marginTop
if (newHeight !== oldHeight) { if (newHeight !== oldHeight) {
itemHeights.set(absoluteIndex, newHeight) itemHeights.set(absoluteIndex, newHeight)
avg.add(newHeight) avg.add(newHeight)
const offset = newHeight - (oldHeight || 0) const offset = newHeight - oldHeight || 0
const groupIndex = absoluteIndex - (absoluteIndex % groupItemCount) const groupIndex = absoluteIndex - (absoluteIndex % groupItemCount)
if (groupHeights.has(groupIndex)) groupHeights.set(groupIndex, groupHeights.get(groupIndex) + offset) if (groupHeights.has(groupIndex)) groupHeights.set(groupIndex, groupHeights.get(groupIndex) + offset)
} }
@ -81,36 +76,25 @@ export const FastListComponent = Component.register('FastList', container => {
container.state.renderedList = list container.state.renderedList = list
} }
container.state.__watch('list', list => { container.state.__watch('list', list => {
console.log('FastList list bound, size:', list?.length, container.id);
listInited = false listInited = false
itemHeights.clear() itemHeights.clear()
groupHeights.clear() groupHeights.clear()
avg.clear() avg.clear()
topMargin = 0
itemMarginTop = null
itemMarginBottom = null
if (!list?.length) return if (!list?.length) return
const listSize = list.length || 0 const listSize = list.length || 0
groupItemCount = Math.ceil(Math.sqrt(listSize)) || 10 groupItemCount = Math.ceil(Math.sqrt(listSize)) || 10
const style = window.getComputedStyle(container) const style = window.getComputedStyle(container)
containerPaddingTop = parseFloat(style.paddingTop) || 0 containerPaddingTop = parseFloat(style.paddingTop)
containerRowGap = parseFloat(style.rowGap) || 0 containerRowGap = parseFloat(style.rowGap) || 0
container.state.renderedList = list.slice(0, Math.min(30, listSize)) || [] container.state.renderedList = list.slice(0, Math.min(30, listSize)) || []
setTimeout(() => {
requestAnimationFrame(() => { // 渲染一批之后立刻计算所有高度
if (listInited || container.state.list !== list) return container.state.list.forEach((item, index) => itemHeights.set(index, avg.get()))
const defaultHeight = avg.get() || 32 for (let i = 0; i < listSize; i += groupItemCount) groupHeights.set(i, Math.min(groupItemCount, listSize - i) * avg.get())
for (let i = 0; i < listSize; i++) {
if (!itemHeights.has(i)) itemHeights.set(i, defaultHeight)
}
for (let i = 0; i < listSize; i += groupItemCount) {
let sum = 0
for (let j = i; j < Math.min(i + groupItemCount, listSize); j++) {
sum += itemHeights.get(j)
}
groupHeights.set(i, sum)
}
listInited = true listInited = true
console.log('FastList inited, calling refresh', container.id);
container.refresh() container.refresh()
}) })
}) })