Render Performance Improvements in PSPDFKit for Web
Here at PSPDFKit, we’re always reviewing our tech to find areas in which we can improve. Recently, we’ve been working to improve PSPDFKit for Web Standalone, our PDF Web SDK that runs completely in the browser.
PSPDFKit for Web Standalone is written in TypeScript, but all the PDF operations are executed in C++. We do this by utilizing a technology called WebAssembly, which allows us to compile our C++ code into a binary format web engines can execute. Being able to run C++ means all our PSPDFKit products can be driven from the same source, further solidifying the stability of the product.
In this particular blog post, I’m going to take you through the journey of analyzing render performance, and I’ll explain our eventual decision to switch from PNG to WebP encoding when rendering for the SDK user interface (UI). I’ll share the numbers that drove the decisions, along with the theories of what these numbers are telling us.
But first, here’s a sneak peek at the improvement. On the left side is a PNG image, and on the right side is a WebP image. The dimensions of the example PDF page are large, which results in it taking some time to render, but overall, this example highlights the speed improvements gained with the encoding switch.
The Render Process
First, it’s important to understand what exactly happens when we request a page to be rendered.
It begins with a request from PSPDFKit for Web to render a PDF page. This request is sent to the C++ PDF engine using bindings that allow TypeScript to talk to WebAssembly. The request includes information like the page index, the desired dimensions of the resulting rendered image, and the format.
Here’s an example of a request type (not real typing, but it gives you an idea of the data):
declare type RenderRequest { pageIndex: number, size: { width: number, height: number }, offset: { x: number, y: number }, format: 'png' | 'webp' | 'bitmap' }
The PDF engine will reply with a buffer holding the requested rendered image in the format specified, and PSPDFKit for Web is then free to do what it would like with the buffer.
But what happens in the black box that is the PDF engine?
First, PDF objects for the page are parsed. Then, objects are rendered to bitmap. Finally, the bitmap is encoded, if required. This flow is simplified, but it’ll suffice for this blog post.
Each of the steps have aspects that can make the render process faster or slower. For example, a page may have hundreds of paths to parse, which would slow down the document parsing. Or, the requested size of the bitmap may be very large, in turn slowing down the render engine.
But what we’re going to look at here is the encoding used to reduce the data passed between WebAssembly and PSPDFKit for Web code.
Why Encode the Rendered Bitmap?
It’s a valid question: If we already have a bitmap, and if we can render that directly to canvas, why encode the data at all?
To explain why we’d want to encode the render, I’ll introduce the first benchmark example.
With each example, we set performance markers to measure the time between when a render request is sent and when the rendered image is seen on the screen. Technically, we use a high-quality tiling system, which render blocks of the page iteratively, so we actually wait for the last tile to be rendered, which is when the page is shown at its highest quality.
Bitmap (ms) | PNG (ms) | WebP (ms) |
---|---|---|
1665 | 2061 | 787 |
755 | 2020 | 788 |
716 | 2198 | 2480 |
714 | 2226 | 777 |
761 | 2209 | 783 |
762 | 2261 | 749 |
1527 | 2186 | 776 |
747 | 2003 | 757 |
707 | 2033 | 780 |
716 | 2103 | 763 |
The test above was performed directly on the PSPDFKit for Web UI to represent real-world numbers, and I used a MacBook Pro 2019 (more on that later). Each format was tested 10 times to average out any variability and anomalies.
The first thing that’s obvious is that PNG is slow! This hasn’t always been the case though. We used to transfer the data from WebAssembly in a different way, which meant transferring large data buffers was slow, and thus transferring large bitmaps was undesirable.
But now that we’ve optimized the data transfer, why not just use bitmaps? Based on the last results, that would be a logical conclusion.
Well sadly, our SDK doesn’t only run on high-powered computer devices. Sometimes, we’re faced with a low-end Android device (yes, I still use one) and the results are much different.
Here are the results using an underpowered Android device. I used a large PDF page to emphasize the speed disparities.
Bitmap (ms) | PNG (ms) | WebP (ms) |
---|---|---|
12323 | 8154 | 6996 |
15060 | 8006 | 6339 |
13934 | 8482 | 6669 |
Now we see the drastic downside to not encoding, likely because we’re passing large buffers around on low-powered devices.
Page Rendering Comparison Results
There are various reasons why WebP is likely a better encoding to use in our rendering process, but let me first further prove that it’s more performant.
Bitmap (ms) | PNG (ms) | WebP (ms) |
---|---|---|
1608 | 1880 | 1565 |
1699 | 2082 | 1544 |
1614 | 1877 | 1541 |
1517 | 1871 | 1526 |
1551 | 1871 | 1531 |
1717 | 1903 | 1576 |
1597 | 1868 | 1529 |
1637 | 1903 | 1572 |
1481 | 2118 | 1577 |
1551 | 1905 | 1603 |
Again, I’m performing tests on my MacBook Pro 2019.
In the first example, we see a pretty standard PDF with a block of text. This is representative of many PDF use cases, which often consist of either simple PDFs or documents with only a few small images.
We can see that not encoding (bitmap) and WebP perform similarly. And again, PNG is lagging behind.
Bitmap (ms) | PNG (ms) | WebP (ms) |
---|---|---|
1560 | 1941 | 956 |
1476 | 1838 | 1117 |
1545 | 1887 | 639 |
1494 | 1793 | 624 |
1531 | 1867 | 619 |
1522 | 2248 | 629 |
1527 | 2501 | 636 |
1482 | 2824 | 585 |
1526 | 2675 | 608 |
1569 | 2618 | 686 |
Next, we have a page with very little text, one image, and a few annotations. Note how the rendering of annotations isn’t taken into account here.
We see a dramatic win for WebP in this instance. And I’ll give reasons for why that is later.
Bitmap (ms) | PNG (ms) | WebP (ms) |
---|---|---|
4875 | 7099 | 3817 |
3841 | 10710 | 3880 |
3112 | 7021 | 4206 |
3108 | 7089 | 5035 |
3090 | 7378 | 3982 |
3100 | 12403 | 3990 |
3173 | 7357 | 5174 |
3047 | 8772 | 4035 |
3099 | 8479 | 3863 |
4534 | 10259 | 4006 |
Lastly, it’s back to our very large PDF page with a single image.
Here, we see bitmaps have slightly better performance, but WebP is still roughly comparable.
Why Is WebP the Answer?
So why does adding an extra operation — encoding a bitmap to WebP — either take the same amount of time to show on the screen, or, as in the example of the underpowered device, even end up faster?
Less Data to Pass
The first theory is that transferring less data across the WebAssembly boundary is faster. We’ve seen this in the past when we’ve optimized boundary crossing, and it’s something we still need to consider when designing code that crosses this boundary.
Passing buffers from one place to the next isn’t free, especially when optimal solutions like C++ move semantics aren’t an option.
Bitmaps can be orders of magnitude larger than encoded images, so if we can perform a fast operation such as encoding, we may see the benefit in reducing the data transferred.
WebP Decoding in the Browser
In 2010, the WebP format was announced, and it was created with the web in mind. Because it supports lossless and lossy encoding, it was touted as an alternative to PNG and JPEG for the web. Even though it’s been 12 years since then, only 5 percent of the web uses WebP. However, that number is growing, and many of the popular sites on the web have now converted to use WebP.
With all this development and focus on WebP for the web, it makes sense that browsers are optimal at decoding and rendering from WebP images. We already have figures to show that decoding WebPs is faster than decoding PNGs.
So in theory, utilizing WebP images is optimal in comparison to other encodings, and possibly even bitmaps, but it’s difficult to produce figures to prove that.
WebP Encoding in C++
In the past, we hadn’t been able to make the WebP project stable under WebAssembly, which is why we hadn’t utilized encoding it. But with the release of version 1.2.1 of the WebP project, our luck changed.
More specifically, we implemented WebP encoding and pushed it through our automated tests and QA procedure, and it passed with flying colors!
With comments from QA like “I love this stuff; it’s cool to see improvements like this,” I’m confident customers will feel the same way.
When it comes to numbers, there were speed increases of up to 50 percent when encoding with WebP vs. PNG, which had a direct contribution to the performance we were seeing.
Conclusion
I hope I’ve managed to convince you that WebP is obviously the way forward for rendering in PSPDFKit for Web. The large speed improvements over PNG, and the stability of performance even on low-end devices, make the change an obvious choice.
It’s worth mentioning that most modern browsers do have support for WebP, but in some instances, we’d have to revert to PNG. However, this is a seamless process, and nothing needs to be changed by the user. To be more specific, IE11 doesn’t support WebP, and Safari has presented some issues with WebP in the past. In the latter case, we added runtime checks to ensure WebP support is fully functional.
Making changes to the encoding within the rendering process is only one of the first improvements we expect to see in PSPDFKit for Web. We’ve identified other areas that can be refined — from document open speed, to large document parsing performance — and we hope to be introducing those improvements soon.
When Nick started tinkering with guitar effects pedals, he didn’t realize it’d take him all the way to a career in software. He has worked on products that communicate with space, blast Metallica to packed stadiums, and enable millions to use documents through Nutrient, but in his personal life, he enjoys the simplicity of running in the mountains.