How we made Avocode faster with concurrent design rendering

Matous RoskovecMatous Roskovec

Designers and developers work with all kinds of tools on all types of platforms, so a proper design hand-off tool needs to work with all primary design tools (Sketch, Adobe XD, Photoshop, Illustrator and Figma) and on all platforms (macOS, Windows, Linux, and the web). Easier said than done.

So how does Avocode do that? It’s magic. No really, it’s a design parsing technology combined with the Monroe rendering engine, written in C++ and compiled to WebAssembly via Emscripten, so the engine works on the server as well as on the client. Building this machine was quite a challenge. Was it a good idea? Yup, it works. Would we start such a project again if we could travel back in time? Not sure.😅

With that in mind, it’s our great pleasure to introduce you to Avocode 3.7 — an update that has cost us the most time, sweat, and merge requests to date. The new Avocode is finally powered by design processing and rendering that happens locally in the app and the reworked Monroe rendering engine that works like other vector-based design tools such as Sketch, Figma or Adobe XD.

Precision, then speed

Up until now, the design canvas in Avocode behaved similarly to the bitmap image editors like Photoshop. When you zoomed in on a design file, it would become pixelated, since we have rendered the design only at 100% at 1x.

There were several reasons for this. In the beginning, Avocode was a developer-first tool that would help you inspect the design — get specs, code, and assets. As long as the exported assets were rendered correctly, the design preview didn’t have to be crystal sharp after going over 100% zoom. However, this changed rapidly as Avocode has become more of a design file hub for teams, where you can not only inspect, but also store files with version history and discuss design feedback. This brought in a wave of designers, stakeholders and agency clients who care very much about how crisp the design looks.

Our priority when building the rendering engine was precision. After we hit 99% design rendering precision across a sample of thousands of design files, we started to look more into a smooth zooming and rendering performance.

Until the 3.7 update, this is what would happen with every design you imported to Avocode: 1. Your design file was uploaded to the server. 2. We parsed its data structure and translated it to our universal design format Octopus so we could render it on any platform. 3. When you opened the design file on the client, Avocode would download the Octopus file (JSON object), bitmaps from the design, and also a rendered preview of the design.

All of it needed to happen before you could start inspecting layers. When you zoomed above 100%, the design would become pixelated. Generating more bitmap previews at higher scale already on the server would be a brute force fix. For example, at 4x scale we would need to render16x the amount of pixels and downloading such a bitmap would significantly decrease the app performance.

In short, we needed something more elegant.

The challenge of web technologies

As pointed out, in the beginning, we need Avocode to run everywhere. This is possible thanks to web technologies (React.js, Node.js, Electron) which naturally come with certain limitations. The main weakness of web technologies such as JavaScript is the single threaded environment. This means that when a user requests an action, a single threaded task needs to be finished before it’s possible to continue working in the app.

Whenever you opened a design in Avocode or requested a rendering change by toggling layers, the rendering task was one — often very long — synchronous call. Since the task wasn’t split into more threads, the user couldn’t interact with the app until the call was finished.

A “quick fix” for this issue was a hybrid rendering architecture that pulled the design preview from the server while enabling local rendering for smaller design adjustments like toggling layers, so we wouldn’t have to download an utterly re-rendered design preview (a PNG or WebP around 5MB). The main problem with this solution was that if you toggled a layer that was covering a significant part of the design, the local rendering could take up to 10s and occasionally even freeze the app.

While web dev technologies don’t enable you to run multiple tasks in different threads simultaneously, the Facebook React.js team is currently working on Concurrent React — a way to split long synchronous tasks from a single thread into smaller chunks. This way you would need to wait only for small bits of the job to be finished. This approach inspired us to fundamentally rework our rendering infrastructure and introduce an entirely local rendering that would split rendering tasks into smaller chunks that still run in one thread.

Old (Avocode 3.6 and older) rendering running in long synchronous calls.

Because the user interactions are unpredictable, we needed a way to find time for the rendering tasks to be completed to avoid lagging. To do that, we wrote an instruction mechanism, a bytecode if you will, that first looks at the design file complexity and generates a list of jobs that need to happen one by one. While we cannot stop the little tasks when the user interacts with the app, we can stop the bytecode interpretation between two tasks and can change the order of what will be rendered next at any time.

New (Avocode 3.7) rendering thread split into short tasks.

Since our cross-format rendering engine needs to draw and replicate everything that can appear in Sketch, Adobe XD, Figma, Photoshop and Illustrator formats correctly, we’re dealing with a very complex composition. We had to figure more than just the architecture of this solution. We had to make it fast.

It took us a while, but the 3.7 update is proof that we can compete with well-established vector editing tools in terms of the design rendering speed.

The new local rendering engine is running whenever you open a design file, zoom, move around or toggle layers. For this to work well, it’s essential to consider many time-related issues. How long should we wait to render something after an interaction? If we run the rendering tasks during the interaction, for example, while you’re zooming, it would quickly become glitchy. Instead, we had to turn on the render a few milliseconds after the interaction. While this might seem odd, it’s exactly how other vector editing tools like Adobe XD work (just try).

Another issue was finding the ultimate performance between how much memory and CPU is left on the computer and the smoothness of design rendering while zooming and moving around.

After a few months of optimization, we try to be at 60fps. When there is not enough memory or CPU, the rendering can slow down to something around 30fps, but luckily — thanks to the concurrent mode — it shouldn’t freeze.

App performance is our number one goal now, so you can expect more improvements coming, for example, we’re waiting for Web Assembly (currently announced) to launch a multiple thread environment in Chrome.

A few performance perks

Faster design loading

From now on, we’re not waiting for bitmaps and the whole design preview to be downloaded from the server to open the design file. This has significantly improved the design file loading performances for all design formats — in some cases even 3x. Once Avocode downloads the design data, the rendering starts and the user can already click around and inspect. Bitmaps are coming along the way. Thanks to this change, we’re finally ready to make zooming razor-sharp while not freezing the app.

Faster design rendering while moving and zooming

We’re also rendering only the part of the design the user sees. Once that is razor sharp, we continue to pre-render other elements of the design so when you move to these parts you’d see them sharp as soon as possible. We’re also loading the bitmaps from the viewport first so you could have them ready for export quickly.

What is Avocode?

It’s an all-in-one tool for teams that want to code and collaborate on UI design files 2x faster.

Sharp vectors scaling

Even at 400% your design files will be razor sharp — that includes vector shapes and bitmaps (depending on the resolution they were imported in to the design tool, e.g., if you added a picture 4000x4000px to Sketch and scaled it down to 1000x1000x, and import the design to Avocode, we’ll still have the bitmap ready for export up to 4000x4000px and you’ll be able to see it sharp at 400% scale). Fonts are sharply scaled up to 400% for Adobe XD designs that were imported via the desktop app. Sharp font scaling for other formats is coming soon.

Faster design import in the desktop app

With the 3.7 update, we’re also introducing local processing of Adobe XD files. That means your file will not have to be uploaded to the server to open it at first (it will be synced later in the background so you or your colleagues could access it from the Avocode web app as well).

FYI, we’re currently working also on local Sketch design processing which will speed up your Sketch import up a notch. Local design processing is also our first step to offering an Enterprise solution for businesses that need to keep their design files within their walls. With this technology, developers will be able to open a design file on any computer without having to upload it to the cloud and without any additional design tool.

Did you like this article? Spread the word!