In episodes 61-69 we created a hex editor, but it was fairly slow when dealing with big files.
So let's start with what we had in episode 69 and let's make it really fast.
Performance problem
Hex editor's performance story had two parts.
Initially, the app was creating DOM for every row, that made startup very slow, but after that it was very smooth as no more updates were needed.
After the change, app created empty placeholder DOM entry for every row, then whenever scrolling happened, it checked which rows needed to display data (on-screen), and which could stay empty (off-screen). Initial render was much faster, but still not amazing. And now scrolling was slow, as Svelte needed to figure out app needed to update.
New solution
Well, but why do we even bother creating placeholder elements? So here's the new idea - size up container to fit all the elements, then only create the ones we need. To simplify the implementation, I just forced every row to be 16px high.
src/Slice.svelte
<script>
import { printf } from "fast-printf"
import AsciiSlice from "./AsciiSlice.svelte"
export let offset
export let rowNumber
export let data
</script>
<div class="row" style={`top: ${16*rowNumber}px`} class:even={rowNumber % 2}>
<span class="offset">{printf("%06d", offset)}</span>
<span class="hex">
{#each {length: 16} as _, i}
<span data-offset={offset + i}>
{data[i] !== undefined ? printf("%02x", data[i]) : " "}
</span>
{/each}
</span>
<AsciiSlice {data} />
</div>
<style>
.row {
position: absolute;
width: 100%;
height: 16px;
}
.even {
background-color: #555;
}
.offset {
margin-right: 0.75em;
}
.hex span:nth-child(4n) {
margin-right: 0.75em;
}
</style>
We only needed to change a few things.
- removed the whole
if visible
logic - every row gets
rowNumber
(which is alwaysoffset/16
right now, but it seems more logical to pass both) - the row is 16px and positioned absolutely based on
rowNumber
- we cannot rely on CSS to do
even
/odd
logic, as we don't know if first actually visible element is odd or even, so we need to manage.even
class ourselves
src/MainView.svelte
<script>
import Slice from "./Slice.svelte"
import { createEventDispatcher } from "svelte"
export let data
let dispatch = createEventDispatcher()
let slices
let main1
let main2
let firstVisible = 0
let lastVisible = 200
$: {
slices = []
for (let i=0; i<data.length; i+=16) {
slices.push({
rowNumber: i/16,
offset: i,
data: data.slice(i, i+16),
})
}
}
$: visibleSlices = slices.slice(firstVisible, lastVisible+1)
$: totalHeight = `height: ${16*slices.length}px`
function onmouseover(e) {
if (!e.target.dataset.offset) {
return
}
dispatch("changeoffset", e.target.dataset.offset)
}
function setVisible() {
let rowHeight = 16
firstVisible = Math.floor(main1.scrollTop / rowHeight)
lastVisible = Math.ceil((main1.scrollTop + main1.clientHeight) / rowHeight)
main2.focus()
}
function init1(node) {
main1 = node
setVisible()
}
function init2(node) {
main2 = node
}
</script>
<div
class="main1"
on:scroll={setVisible}
use:init1
>
<div
class="main2"
on:mouseover={onmouseover}
style={totalHeight}
use:init2
tabindex="-1"
>
{#each visibleSlices as slice (slice.offset)}
<Slice {...slice} />
{/each}
</div>
</div>
<svelte:window on:resize={setVisible} />
<style>
.main1 {
flex: 1 1 auto;
overflow-y: auto;
width: 100%;
}
.main2 {
position: relative;
}
</style>
This is possibly not the most tidy code, there's external main1
scrollable viewport div with size flexing to available space, and inner main2
div sized to fit all rows.
There's a few tricks here. We need to add tabindex="-1"
on the inner main2
and keep running main2.focus()
after every scroll, otherwise keyboard navigation wouldn't work. In previous version what was focused were individual rows, but now we delete them, and that would remove focus completely instead of moving it to main2
. By forcing focus to stay on main2
, keyboard navigation works. This isn't the most elegant solution, but nothing else is selectable, so it works. In more complex app, we should only steal focus if it belonged to a row that was about to be deleted.
When we iterate with {#each visibleSlices as slice (slice.offset)}
, we need to tell Svelte to identify rows by slice.offset
, instead of by loop index. Otherwise, we'd need to tell AsciiSlice
components to recompute their data every time, instead of only on creation as it does now.
And of course we need to tag main2
as position: relative
, to let the browser know that position: absolute
of Slice
components is based on main2
, not on the main window.
Results
Here's the results:
In the next episode we'll write some games.
As usual, all the code for the episode is here.
Top comments (0)