7

I've been implementing a chat client in Electron (Chrome) and React. Our top priority is speed. It behooves us, then, to use a virtualized list component (also known as "buffered render" or "window render"). We've explored react-virtualized, react-window, and react-infinite, among others.

One issue all these components have in common is that if supporting list elements of variable heights, the heights need to be known in advance. Now, some chats are very long, and others are very short, so that presents a challenge for us. (Images and video are easy thanks to EXIF data and ffprobe).

So, we're faced with the challenge of measuring heights while also straining to be extremely performant. One obvious technique is to put the elements in a browser container off-viewport, perform the measurements, and then render the list. But that hurts us on the performance requirement aspect. Software like react-virtualized/CellMeasurer (which is no longer maintained by the original author) and react-window make us of this technique, built in to the library, but performance is somewhat slow as well as unreliable. A similar idea that might be more performant would be to use a background Electron Browser window to do the rendering and measuring, but my intuition is that wouldn't be that much faster.

I submit that there must be some solved way to figure out string height in advance, according to word wrap, max width, and font rules.

My current idea is to use a library like string-pixel-width in order to calculate row heights as soon as we get the text data through our API. Basically, the library uses this piece of code to generate a map of character widths [*]. Then, once we know how wide each text, we separate each line when it maxes out the computed max row width, and finally infer list element height through row count. It's going to require a little bit of algorithmic fiddling due to break-word but there are libraries to help with that – css-line-break seems promising.

[*] We would have to modify it a bit to account for all Unicode character ranges, but that is trivial.

Some options I haven't fully explored yet include the python weasyprint project and the facebook-yoga project. I'm open to your ideas!

Zack Burt
  • 8,257
  • 10
  • 53
  • 81
  • 1
    I'll keep this suggestion in the comments until I hear your feedback. You can use the advance text measurement functionality of chromium on the canvas https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics Especially since the text rendering is the same for canvas and html (eg: both use Skia) I'm also waiting for this to be implemented in chrome https://github.com/WICG/virtual-scroller/blob/master/README.md which makes virtualization much easier in general. – Hans Koch Dec 25 '19 at 11:27
  • https://chromestatus.com/feature/5673195159945216 also for the status tracking in chromium – Hans Koch Dec 25 '19 at 11:28
  • Hi Hans, I wanted to quickly acknowledge your answer. Some notes in advance: * We are using Electron, so I don't know if it's possible to enable experimental flags or whether the experimental aspects are necessary for what we're trying to accomplish – seems base functionality has been in since Chrome 4; * Using `word-wrap` to break the text is essential – unclear whether that's taken into account with measureText(); * Is it possible to do within node, perhaps with Headless Chrome or some lib that implements canvas? Expect detailed feedback mid-day tomorrow (UTC-5). Merry Christmas (UTC+0)! – Zack Burt Dec 25 '19 at 15:25
  • 1
    Luckily Electron allows enabling flags, we use this features in our text heavy electron app ourself. It's UTC+0 here and I'll compose a full answer ASAP. But generally word-wraps have to be manually handles, we used pixijs's text functionality for that, which can operate in a standalone. Also, Electron has access to the canvas on the render process, and out of performance reasons I would always recommend reducing IPC calls to a minimum (Serialization is slow). If your logic layer works only in the main process take a look at `node-canvas` but maybe think about moving the code into a preloader. – Hans Koch Dec 25 '19 at 16:29
  • If you want more detail about the preloader script based architecture feel free to hit me up on the electron slack channel or twitter @Hammster1911 – Hans Koch Dec 25 '19 at 16:32
  • Hi Hans! Thanks for making yourself available on Electron and Twitter. I just sent you a follow. But would you mind posting about the details of the preloader script based architecture here as an answer, along with the details (flag) to enable experimental features with Electron (and ideally anything with pixijs)? This would give me the opportunity as well to mark this as the accepted answer! – Zack Burt Dec 26 '19 at 15:38

1 Answers1

4

Using the canvas capabilities to measure text could solve this problem in a performant way.

Electrons canvas text is calculated the same as the regular text, there are some diffrences in rendering though especially in reguard of anti-aliasing but that does not affect the calculation.

You can get the TextMetrics from any text with

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

// Set your font parameters
// Docs: https://developer.mozilla.org/en-US/docs/Web/CSS/font
ctx.font = "30px Arial";

// returns a TextMetrics object
// Docs: https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics
const text = ctx.measureText('Hello world')

This does not include line breaks and word wraps, for this feature I would recommend you to use the text package from pixijs, it uses this method already. In addition you could fork the source (MIT licence) and modify it for additional performance by enabling the experimental chromium TextMetrics features in electron and make use of it.

This can be done when creating a window

new BrowserWindow({
  // ... rest of your window config ...

  webPreferences: {
    experimentalFeatures: true
  }
})

Now to the part I mentioned in the comments since I don't know your codebase, your calculations and everything should be happening in the Render Process. If that is not the case you definitely should move your code from the main process over to the render process, if you do file access operations or anything node specific you should still do this but in a so-called preload script

it's a additional parameter in the webPreferences

webPreferences: {
  preload: path.join(__dirname, 'preload.js')
  experimentalFeatures: true
}

In this script you have full access to node including native node modules without the use of IPC calls. The reason I discourage IPC calls for any type of function that gets called multiple times is that it is slow by nature, you need to serialize/deserialize to make them work. The default behaviour for electron is even worse since it uses JSON, except you use ArrayBuffers.

Hans Koch
  • 4,283
  • 22
  • 33
  • Which part of this solution requires `experimentalFeatures` to be turned on? – snwflk Dec 29 '19 at 02:04
  • This would be for the optimization of the measurement. Either when modifing the pixijs code or using a custom textwrap. Especially for text with multiple line heigts due to certain symbols. – Hans Koch Dec 29 '19 at 02:35