1
0
Fork 0
mirror of synced 2024-09-01 18:21:32 +12:00

Optimise scrolling and virtual rendering performance

This commit is contained in:
Andrew Kingston 2023-02-25 16:33:20 +00:00
parent 4d3f669ae7
commit 0eadca9acb
4 changed files with 81 additions and 68 deletions

View file

@ -35,7 +35,8 @@
const tableId = writable(table?.tableId) const tableId = writable(table?.tableId)
const changeCache = writable({}) const changeCache = writable({})
const newRows = writable([]) const newRows = writable([])
const visibleCells = writable({ y: [0, 0], x: [0, 0] }) const visibleRows = writable([0, 0])
const visibleColumns = writable([0, 0])
// Build up spreadsheet context and additional stores // Build up spreadsheet context and additional stores
const context = { const context = {
@ -49,7 +50,8 @@
changeCache, changeCache,
newRows, newRows,
cellHeight, cellHeight,
visibleCells, visibleRows,
visibleColumns,
} }
const { reorder, reorderPlaceholder } = createReorderStores(context) const { reorder, reorderPlaceholder } = createReorderStores(context)
const resize = createResizeStore(context) const resize = createResizeStore(context)
@ -67,7 +69,7 @@
$: rowCount = $rows.length $: rowCount = $rows.length
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length $: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
$: updateSortedRows($fetch, $newRows) $: updateSortedRows($fetch, $newRows)
$: visibleRows = $rows.slice($visibleCells.y[0], $visibleCells.y[1]) $: renderedRows = $rows.slice($visibleRows[0], $visibleRows[1])
const createFetch = datasource => { const createFetch = datasource => {
return fetchData({ return fetchData({
@ -160,16 +162,16 @@
const updateSortedRows = (unsortedRows, newRows) => { const updateSortedRows = (unsortedRows, newRows) => {
let foo = unsortedRows.rows let foo = unsortedRows.rows
for (let i = 0; i < 10; i++) { for (let i = 0; i < 3; i++) {
foo = foo.concat(foo.map(x => ({ ...x, _id: x._id + "x" }))) foo = foo.concat(foo.map(x => ({ ...x, _id: x._id + "x" })))
} }
// let sortedRows = foo.slice() let sortedRows = foo.slice()
// sortedRows.sort((a, b) => { sortedRows.sort((a, b) => {
// const aIndex = newRows.indexOf(a._id) const aIndex = newRows.indexOf(a._id)
// const bIndex = newRows.indexOf(b._id) const bIndex = newRows.indexOf(b._id)
// return aIndex < bIndex ? -1 : 1 return aIndex < bIndex ? -1 : 1
// }) })
$rows = foo.slice() $rows = sortedRows
} }
// API for children to consume // API for children to consume
@ -229,8 +231,8 @@
</div> </div>
<!-- All real rows --> <!-- All real rows -->
{#each visibleRows as row, rowIdx (row._id)} {#each renderedRows as row, rowIdx (row._id)}
<SpreadsheetRow {row} rowIdx={rowIdx + $visibleCells.y[0]} /> <SpreadsheetRow {row} rowIdx={rowIdx + $visibleRows[0]} />
{/each} {/each}
<!-- New row placeholder --> <!-- New row placeholder -->

View file

@ -1,8 +1,16 @@
<script> <script>
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import { domDebounce } from "../../../utils/domDebounce"
const { columns, selectedCellId, rand, visibleCells, cellHeight, rows } = const {
getContext("spreadsheet") columns,
selectedCellId,
rand,
visibleRows,
visibleColumns,
cellHeight,
rows,
} = getContext("spreadsheet")
const padding = 180 const padding = 180
@ -12,7 +20,8 @@
let scrollLeft = 0 let scrollLeft = 0
let scrollTop = 0 let scrollTop = 0
$: computeVisibleCells($columns, scrollLeft, scrollTop, width, height) $: updateVisibleRows($columns, scrollTop, height)
$: updateVisibleColumns($columns, scrollLeft, width)
$: contentHeight = ($rows.length + 2) * cellHeight + padding $: contentHeight = ($rows.length + 2) * cellHeight + padding
$: contentWidth = computeContentWidth($columns) $: contentWidth = computeContentWidth($columns)
$: horizontallyScrolled = scrollLeft > 0 $: horizontallyScrolled = scrollLeft > 0
@ -26,35 +35,52 @@
} }
// Store the current scroll position // Store the current scroll position
const handleScroll = e => { // let lastTop
// Only update scroll offsets when a sizable change happens // let lastLeft
if (Math.abs(scrollTop - e.target.scrollTop) > 10) { // let ticking = false
scrollTop = e.target.scrollTop // const handleScroll = e => {
} // lastTop = e.target.scrollTop
if (Math.abs(scrollLeft - e.target.scrollLeft) > 10) { // lastLeft = e.target.scrollLeft
scrollLeft = e.target.scrollLeft // if (!ticking) {
} // ticking = true
if (e.target.scrollLeft === 0) { // requestAnimationFrame(() => {
scrollLeft = 0 // if (Math.abs(lastTop - scrollTop) > 100) {
} // scrollTop = lastTop
} // }
// if (lastLeft === 0 || Math.abs(lastLeft - scrollLeft) > 100) {
// scrollLeft = lastLeft
// }
// ticking = false
// })
// }
// }
const computeVisibleCells = ( const handleScroll = domDebounce(
columns, ({ left, top }) => {
scrollLeft, if (Math.abs(top - scrollTop) > 100) {
scrollTop, scrollTop = top
width, }
height if (left === 0 || Math.abs(left - scrollLeft) > 100) {
) => { scrollLeft = left
}
},
e => ({ left: e.target.scrollLeft, top: e.target.scrollTop })
)
const updateVisibleRows = (columns, scrollTop, height) => {
if (!columns.length) { if (!columns.length) {
return return
} }
// Compute row visibility // Compute row visibility
const rows = Math.ceil(height / cellHeight) + 8 const rows = Math.ceil(height / cellHeight) + 8
const firstRow = Math.max(0, Math.floor(scrollTop / cellHeight) - 4) const firstRow = Math.max(0, Math.floor(scrollTop / cellHeight) - 4)
const visibleRows = [firstRow, firstRow + rows] visibleRows.set([firstRow, firstRow + rows])
}
const updateVisibleColumns = (columns, scrollLeft, width) => {
if (!columns.length) {
return
}
// Compute column visibility // Compute column visibility
let startColIdx = 1 let startColIdx = 1
let rightEdge = columns[1].width let rightEdge = columns[1].width
@ -68,12 +94,7 @@
leftEdge += columns[endColIdx]?.width leftEdge += columns[endColIdx]?.width
endColIdx++ endColIdx++
} }
const visibleColumns = [Math.max(1, startColIdx - 2), endColIdx + 2] visibleColumns.set([Math.max(1, startColIdx - 2), endColIdx + 2])
visibleCells.set({
x: visibleColumns,
y: visibleRows,
})
} }
onMount(() => { onMount(() => {
@ -87,26 +108,15 @@
observer.disconnect() observer.disconnect()
} }
}) })
let sheetStyles = ""
let left = 0
for (let i = 0; i < 20; i++) {
if (i === 1) {
left += 40
}
sheetStyles += `--col-${i}-width:${160}px; --col-${i}-left:${left}px;`
left += 160
}
</script> </script>
<div <div
bind:this={ref} bind:this={ref}
class="spreadsheet" class="spreadsheet"
class:horizontally-scrolled={horizontallyScrolled} class:horizontally-scrolled={horizontallyScrolled}
on:scroll={handleScroll}
on:click|self={() => ($selectedCellId = null)} on:click|self={() => ($selectedCellId = null)}
id={`sheet-${rand}-body`} id={`sheet-${rand}-body`}
style={sheetStyles} on:scroll={handleScroll}
> >
<div <div
class="content" class="content"
@ -119,10 +129,10 @@
<style> <style>
.spreadsheet { .spreadsheet {
display: block; display: block;
overflow: auto;
height: 800px; height: 800px;
position: relative; position: relative;
cursor: default; cursor: default;
overflow: auto;
} }
.content { .content {
min-width: 100%; min-width: 100%;

View file

@ -1,7 +1,6 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import SpreadsheetCell from "./SpreadsheetCell.svelte" import SpreadsheetCell from "./SpreadsheetCell.svelte"
import SpacerCell from "./SpacerCell.svelte"
import OptionsCell from "./cells/OptionsCell.svelte" import OptionsCell from "./cells/OptionsCell.svelte"
import DateCell from "./cells/DateCell.svelte" import DateCell from "./cells/DateCell.svelte"
import MultiSelectCell from "./cells/MultiSelectCell.svelte" import MultiSelectCell from "./cells/MultiSelectCell.svelte"
@ -20,16 +19,16 @@
selectedRows, selectedRows,
changeCache, changeCache,
spreadsheetAPI, spreadsheetAPI,
visibleCells, visibleColumns,
cellHeight, cellHeight,
} = getContext("spreadsheet") } = getContext("spreadsheet")
$: rowSelected = !!$selectedRows[row._id] $: rowSelected = !!$selectedRows[row._id]
$: rowHovered = $hoveredRowId === row._id $: rowHovered = $hoveredRowId === row._id
$: data = { ...row, ...$changeCache[row._id] } $: data = { ...row, ...$changeCache[row._id] }
$: visibleColumns = [ $: renderedColumns = [
$columns[0], $columns[0],
...$columns.slice($visibleCells.x[0], $visibleCells.x[1]), ...$columns.slice($visibleColumns[0], $visibleColumns[1]),
] ]
$: containsSelectedCell = $selectedCellId?.split("-")[0] === row._id $: containsSelectedCell = $selectedCellId?.split("-")[0] === row._id
@ -77,7 +76,7 @@
</span> </span>
{/if} {/if}
</SpreadsheetCell> </SpreadsheetCell>
{#each visibleColumns as column (column.name)} {#each renderedColumns as column (column.name)}
{@const cellIdx = `${row._id}-${column.name}`} {@const cellIdx = `${row._id}-${column.name}`}
<SpreadsheetCell <SpreadsheetCell
{rowSelected} {rowSelected}
@ -116,7 +115,7 @@
z-index: 1; z-index: 1;
} }
.row :global(>:last-child) { .row :global(> :last-child) {
border-right-width: 1px; border-right-width: 1px;
} }
</style> </style>

View file

@ -1,12 +1,14 @@
export const domDebounce = callback => { export const domDebounce = (callback, extractParams = x => x) => {
let active = false let active = false
return e => { let lastParams
return (...params) => {
lastParams = extractParams(...params)
if (!active) { if (!active) {
window.requestAnimationFrame(() => { active = true
callback(e) requestAnimationFrame(() => {
callback(lastParams)
active = false active = false
}) })
active = true
} }
} }
} }