Compare commits
No commits in common. "main" and "v1.0.0-beta.1" have entirely different histories.
main
...
v1.0.0-bet
@ -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
12
TEST.md
@ -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
39
dist/base.js
vendored
@ -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
2
dist/base.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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",
|
||||||
|
|||||||
42
src/list.js
42
src/list.js
@ -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()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user