You might have noticed that recent versions improved JShelter performance. This blog post explains the improvements in more detail and contains graphs. The improvements are based on the bachelor thesis of Martin Zmitko. If you are interested in this topic, you will find more information in the thesis. We thank Martin for his work and his proposals.
1. The code inserted into each page
Archaic versions of JShelter (at that time JavaScript Restrictor) generated the wrapping code during each page load (in so-called content scripts). However, we still needed to solve the reliable code injection at that time. We wanted the lowest amount of work in content script. So, JShelter started to generate the code in the background and send the generated code to the content script.
Starting from 0.5, NSCL solved the reliable code injection. The preferred and fast
solution is to inject the configuration in the BeforeNavigate
event handler. However, there is a race
condition between the BeforeNavigate
event and document_start
phase of the page load. If the
script detects that the configuration is not available during the document_start
, it initiates a
synchronous request to retrieve the configuration before page scripts start running.
However, Martin realized that a synchronous request takes a long time. Moreover, he confirmed our old observations that the synchronous request is needed very often. The time needed to process the configuration increases linearly with the size of the configuration. JShelter used to inject 572kB of code in the default configuration. By shifting the code generation process back to content scripts, we decreased the configuration size to 21.2kB.
During the work, we also optimized the code-generating process and eliminated duplicates in the code as well as Firefox-specific code in Chromium-based browsers.
2. Improvements to little-lies
As you probably know, the anti-fingerprinting code modifies the results of some APIs with
little lies. However, that approach is performance-heavy for some APIs. The most critical are APIs
that read from canvas (readPixels
and toDataURL
) and AudioBuffer.getChannelData
. For example,
the original getChannelData
passes a reference to the underlying buffer, so the browser does not
need to do any computation. But JShelter needs to copy each item, determine how to apply the lies
(ensuring consistent lies to the same data) and modify selected items.
Martin discovered that the JShelter modifications to readPixels
, toDataURL
, and getChannelData
can benefit from a different iterator. More importantly, Martin proposed to translate the code to
WebAssembly, which runs much faster.
3. Improvements to FPD
Fingerprint detector collects information on each call of the APIs that are often misused for fingerprinting. Martin discovered that some serializations performed during its operations are not really needed.
4. Other optimizations
Martin also implemented several other performance improvements, some aiming at NSCL and not only
JShelter. For example, NSCL included a JavaScript library to compute SHA-256, while native
SubtleCrypto
implementation is faster.
5. Results
First, let us have a look at Firefox and the improvements in 2D Canvas in getImageData()
(note
that the y-axis is logarithmic):
As expected, the optimized implementation is slower than the original because it needs much more work. Even so, the performance hit is several magnitudes lower than the hit in 0.12.2.
Now, let us have a look the improvements in 3D Canvas in readPixels()
(this time, we show the
graph with the linear y-axis, the shape of the points depicting the performance is similar for
2D and 3D canvas):
See that the original implementation quickly leaves the plotted range. Its performance hit was more significant than the 2D version while starting from 0.14, the performance hit of 2D and 3D canvases are comparable.
Firefox implementation of getChannelData
has a negligible running time (almost 0). The following figure shows that the little lies are computed much quicker (about two orders of magnitude), but the impact is still significant. Note that the y-axis is again logarithmic.
Martin also developed performance tests based on Google Lighthouse that run in Chrome. We tested on 46 pages from the 100 of Tranco and give the performance score below. The performance score approximates how users perceive the loading speed of the visited page. The 25th percentile of all pages should receive a score of 50.
The average performance score of all tested pages was 66 without JShelter. When tested with JShelter 0.12.2, the score dropped to 62.5. The average of all tested pages raised to 64 with JShelter 0.15.1. The performance score was the same or better in JShelter 0.15.1 compared to 0.12.2 on 33 pages. It increased on 18 pages.