前言
由于svelte solid 两大无虚拟DOM框架,由于其性能好,在前端越来越有影响力。
因此本次想要验证,这三个框架关于实现表格虚拟滚动的性能。
比较版本
- vue@3.4.21
- svelte@4.2.12
- solid-js@1.8.15
比较代码
这里使用了我的 stk-table-vue(npm) 中实现虚拟滚动主要代码。
StkTable.vue
<script setup>
import { onMounted, ref, computed } from 'vue';
const props = defineProps({
virtual: {
type: Boolean,
default: true
},
columns: {
type: Array
},
dataSource: {
type: Array
}
})
const tableContainer = ref();
const virtualScroll = ref({
containerHeight: 0,
startIndex: 0, // 数组开始位置
rowHeight: 28,
offsetTop: 0, // 表格定位上边距
scrollTop: 0, // 纵向滚动条位置,用于判断是横向滚动还是纵向
});
const dataSourceCopy = computed(() => [...props.dataSource]);
const virtual_pageSize = computed(() => Math.ceil(virtualScroll.value.containerHeight / virtualScroll.value.rowHeight) + 1); // 这里最终+1,因为headless=true无头时,需要上下各预渲染一行。
const virtual_on = computed(() => props.virtual && dataSourceCopy.value.length > virtual_pageSize.value * 2)
const virtual_dataSourcePart = computed(() => virtual_on.value
? dataSourceCopy.value.slice(virtualScroll.value.startIndex, virtualScroll.value.startIndex + virtual_pageSize.value)
: dataSourceCopy.value)
const virtual_offsetBottom = computed(() => virtual_on.value ? (dataSourceCopy.value.length - virtualScroll.value.startIndex - virtual_dataSourcePart.value.length) * virtualScroll.value.rowHeight : 0)
onMounted(() => {
initVirtualScroll();
})
/**
* 初始化虚拟滚动参数
* @param {number} [height] 虚拟滚动的高度
*/
function initVirtualScroll(height) {
initVirtualScrollY(height);
// this.initVirtualScrollX();
}
/**
* 初始化Y虚拟滚动参数
* @param {number} [height] 虚拟滚动的高度
*/
function initVirtualScrollY(height) {
if (virtual_on.value) {
virtualScroll.value.containerHeight = typeof height === 'number' ? height : tableContainer.value?.offsetHeight;
updateVirtualScrollY(tableContainer.value?.scrollTop);
}
}
/** 通过滚动条位置,计算虚拟滚动的参数 */
function updateVirtualScrollY(sTop = 0) {
const { rowHeight } = virtualScroll.value;
const startIndex = Math.floor(sTop / rowHeight);
Object.assign(virtualScroll.value, {
startIndex,
offsetTop: startIndex * rowHeight, // startIndex之前的高度
});
}
function onTableScroll(e) {
if (!e?.target) return;
// 此处可优化,因为访问e.target.scrollXX消耗性能
const { scrollTop, scrollLeft } = e.target;
// 纵向滚动有变化
if (scrollTop !== virtualScroll.value.scrollTop) virtualScroll.value.scrollTop = scrollTop;
if (virtual_on.value) {
updateVirtualScrollY(scrollTop);
}
}
</script>
<template>
<div class="stk-table" ref="tableContainer" @scroll="onTableScroll">
<table class="stk-table-main">
<thead>
<tr>
<th v-for="col in columns" :key="col.dataIndex" :data-col-key="col.dataIndex">{{ col.title || '--' }}
</th>
</tr>
</thead>
<tbody>
<tr :style="{ height: `${virtualScroll.offsetTop}px` }"></tr>
<tr v-for="row in virtual_dataSourcePart" :key="row.id">
<td v-for="col in columns" :key="col.dataIndex">{{ row[col.dataIndex] || '--' }}</td>
</tr>
<tr :style="{ height: `${virtual_offsetBottom}px` }"></tr>
</tbody>
</table>
</div>
</template>
StkTable.svelte
<script>
import { onMount } from 'svelte';
import '../stk-table/stk-table.less';
export let style = '';
export let virtual = true;
let tableContainer;
let virtualScroll = {
containerHeight: 0,
startIndex: 0, // 数组开始位置
rowHeight: 28,
offsetTop: 0, // 表格定位上边距
scrollTop: 0, // 纵向滚动条位置,用于判断是横向滚动还是纵向
};
export let columns = [
{ dataIndex: 'id', title: 'ID' },
{ dataIndex: 'name', title: 'Name' },
];
export let dataSource = [];
$: dataSourceCopy = [...dataSource];
/** 数据量大于2页才开始虚拟滚动*/
/** 虚拟滚动展示的行数 */
$: virtual_pageSize = Math.ceil(virtualScroll.containerHeight / virtualScroll.rowHeight) + 1; // 这里最终+1,因为headless=true无头时,需要上下各预渲染一行。
$: virtual_on = virtual && dataSourceCopy.length > virtual_pageSize * 2;
/** 虚拟滚动展示的行 */
$: virtual_dataSourcePart = virtual_on
? dataSourceCopy.slice(virtualScroll.startIndex, virtualScroll.startIndex + virtual_pageSize)
: dataSourceCopy;
/** 虚拟表格定位下边距*/
$: virtual_offsetBottom = virtual_on ?(dataSourceCopy.length - virtualScroll.startIndex - virtual_dataSourcePart.length) * virtualScroll.rowHeight : 0;
onMount(() => {
initVirtualScroll();
});
/**
* 初始化虚拟滚动参数
* @param {number} [height] 虚拟滚动的高度
*/
function initVirtualScroll(height) {
initVirtualScrollY(height);
// this.initVirtualScrollX();
}
/**
* 初始化Y虚拟滚动参数
* @param {number} [height] 虚拟滚动的高度
*/
function initVirtualScrollY(height) {
if(virtual_on){
virtualScroll.containerHeight = typeof height === 'number' ? height : tableContainer?.offsetHeight;
virtualScroll = virtualScroll;
updateVirtualScrollY(tableContainer?.scrollTop);
}
}
/** 通过滚动条位置,计算虚拟滚动的参数 */
function updateVirtualScrollY(sTop = 0) {
const { rowHeight } = virtualScroll;
const startIndex = Math.floor(sTop / rowHeight);
Object.assign(virtualScroll, {
startIndex,
offsetTop: startIndex * rowHeight, // startIndex之前的高度
});
}
function onTableScroll(e) {
if (!e?.target) return;
// 此处可优化,因为访问e.target.scrollXX消耗性能
const { scrollTop, scrollLeft } = e.target;
// 纵向滚动有变化
if (scrollTop !== virtualScroll.scrollTop) virtualScroll.scrollTop = scrollTop;
if (virtual_on) {
updateVirtualScrollY(scrollTop);
}
}
</script>
<div class="stk-table" bind:this={tableContainer} {style} on:scroll={onTableScroll}>
<table class="stk-table-main">
<thead>
<tr>
{#each columns as col (col.dataIndex)}
<th data-col-key={col.dataIndex}>{col.title || '--'}</th>
{/each}
</tr>
</thead>
<tbody>
<tr style="height: {`${virtualScroll.offsetTop}px`}"></tr>
{#each virtual_dataSourcePart as row (row.id)}
<tr>
{#each columns as col (col.dataIndex)}
<td>{row[col.dataIndex] || '--'}</td>
{/each}
</tr>
{/each}
<tr style="height: {`${virtual_offsetBottom}px`}"></tr>
</tbody>
</table>
</div>
StkTable.jsx (solid-js)
import { For, createSignal, onMount } from "solid-js";
import '../stk-table/stk-table.less';
export function StkTable(props) {
let tableContainer;
const [virtualScroll, setVirtualScroll] = createSignal({
containerHeight: 0,
startIndex: 0, // 数组开始位置
rowHeight: 28,
offsetTop: 0, // 表格定位上边距
scrollTop: 0, // 纵向滚动条位置,用于判断是横向滚动还是纵向
});
const [dataSourceCopy, setDataSourceCopy] = createSignal([]);
const virtual_pageSize = () => {
const vs = virtualScroll();
return Math.ceil(vs.containerHeight / vs.rowHeight) + 1
}; // 这里最终+1,因为headless=true无头时,需要上下各预渲染一行。
const virtual_on = () => props.virtual && dataSourceCopy().length > virtual_pageSize() * 2;
/** 虚拟滚动展示的行 */
const virtual_dataSourcePart = () => {
const vs = virtualScroll();
const pageSize = virtual_pageSize();
console.log(vs, pageSize)
return virtual_on()
? dataSourceCopy().slice(vs.startIndex, vs.startIndex + pageSize)
: dataSourceCopy()
};
/** 虚拟表格定位下边距*/
const virtual_offsetBottom = () => virtual_on() ? (dataSourceCopy().length - virtualScroll().startIndex - virtual_dataSourcePart().length) * virtualScroll().rowHeight : 0;
onMount(() => {
setDataSourceCopy([...props.dataSource]);
initVirtualScroll();
});
/**
* 初始化虚拟滚动参数
* @param {number} [height] 虚拟滚动的高度
*/
function initVirtualScroll(height) {
initVirtualScrollY(height);
// this.initVirtualScrollX();
}
/**
* 初始化Y虚拟滚动参数
* @param {number} [height] 虚拟滚动的高度
*/
function initVirtualScrollY(height) {
if (virtual_on()) {
const vs = virtualScroll()
vs.containerHeight = typeof height === 'number' ? height : tableContainer?.offsetHeight;
setVirtualScroll(vs);
updateVirtualScrollY(tableContainer?.scrollTop);
}
}
/** 通过滚动条位置,计算虚拟滚动的参数 */
function updateVirtualScrollY(sTop = 0) {
let vs = virtualScroll();
const startIndex = Math.floor(sTop / vs.rowHeight);
Object.assign(vs, {
startIndex,
offsetTop: startIndex * vs.rowHeight, // startIndex之前的高度
});
setVirtualScroll({...vs});// 必须扩展运算,否则不触发更新
}
function onTableScroll(e) {
if (!e?.target) return;
// 此处可优化,因为访问e.target.scrollXX消耗性能
const { scrollTop, scrollLeft } = e.target;
const vs = virtualScroll()
// 纵向滚动有变化
if (scrollTop !== vs.scrollTop) {
vs.scrollTop = scrollTop;
setVirtualScroll(vs);
}
if (virtual_on()) {
updateVirtualScrollY(scrollTop);
}
}
return <div class="stk-table" ref={tableContainer} style={props.style} onScroll={onTableScroll}>
<table class="stk-table-main">
<thead>
<tr>
<For each={props.columns}>{
(col) =>
<th data-col-key={col.dataIndex}>{col.title || '--'}</th>
}</For>
</tr>
</thead>
<tbody>
<tr style={{ height: `${virtualScroll().offsetTop}px` }}></tr>
<For each={virtual_dataSourcePart()}>{
(row) =>
<tr>
<For each={props.columns}>{
(col) => <td>{row[col.dataIndex] || '--'}</td>
}</For>
</tr>
}</For>
<tr style={{ height: `${virtual_offsetBottom()}px` }}></tr>
</tbody>
</table>
</div>
}
style.less
src/StkTable/style.less · JA+/stk-table-vue
性能比较
比较虚拟滚动性能。
通过长按键盘↓键滚动,且将电脑cpu速度调至减速6x
观察浏览器开发者面板performance标签下的任务耗时。
vue任务
svelte任务
solid任务
结论
观察solid任务的超时情况(红色部分),大体比vue与svelte要少。
solid与svelte 相较vue在虚拟滚动情况下,未能观察到明显优势。