Electron Adventures: Episode 84: High Performance Hex Editor
Tags: javascript, electron, svelte
Published: 2021-10-31 20:01:43

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`

```html
<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 always `offset/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`

```html
<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:

![Episode 84 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fvrf26pzk3rmr34p2xwm.png)

In the next episode we'll write some games.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-84-high-performance-hex-editor).