Modern Web Frameworks A Comparison of Rendering Performance
Modern Web Frameworks A Comparison of Rendering Performance
Rendering Performance
1
Intruder Systems Ltd, London, UK
2
University of Helsinki, Department of Computer Science, Helsinki, Finland
E-mail: [email protected]; [email protected];
[email protected]
∗
Corresponding Author
Abstract
Recent years have seen the rise of a new generation of UI frameworks for web
application development. These frameworks differ from previous generations
of JavaScript frameworks in that they define a declarative application devel-
opment model, where transitions in the state of the UI are managed by the
framework. This potentially greatly simplifies application development, but
requires the framework to implement a rendering strategy which translates
changes in application state into changes in the state of the UI. The perfor-
mance characteristics of these rendering strategies have thus far been poorly
studied.
In this article, we describe the rendering strategies used in the frameworks
Angular, React, Vue, Svelte and Blazor, which represent some of the most
influential and widely used modern web frameworks. We find significant
differences in the scaling of costs in their rendering strategies with poten-
tially equally significant practical performance implications. To verify these
differences, we implement a number of benchmarks that measure the scaling
of rendering costs as an application grows in complexity.
1 Introduction
The use of JavaScript on the web has become ubiquitous, with up to 97% of
websites today using JavaScript, up from 88% a decade ago [16]. Over the
same timeframe, there has been a change in how JavaScript is used: whereas
ten years ago up to 60% of websites used JavaScript without the help of any
libraries or frameworks, today less than 20% do so [17].
One reason for the rise of scripting is that it enables rich, responsive user
interfaces for web applications. Without scripting, all transitions in UI state
require page navigation [31], a process that involves resource fetching and
a full reconstruction of the UI for every transition [4] (Figure 1). This is
highly inefficient, particularly for small transitions. Using DHTML [33], a
set of technologies which together enable dynamic modification of a doc-
ument, the UI can be modified incrementally. When a UI state transition
requires resource loading, this can be done asynchronously using the AJAX
pattern [31].
At the core of DHTML is the Domain Object Model (DOM) [5]. DOM
describes the structure of a document as a tree of nodes, and provides a
set of APIs which can be used to dynamically modify that structure. DOM
APIs are imperative, consisting of functions that enable querying, adding
and removing nodes as well as modifying node attributes. Using DOM APIs,
any DOM node tree which can be parsed from an HTML source can also be
constructed using JavaScript.
A pitfall of DHTML is that DOM APIs are error-prone to use [28]. This
may partly explain the increase in the use of JavaScript libraries, as many
of them provide utilities for DHTML and AJAX, often providing library
Modern Web Frameworks: A Comparison of Rendering Performance 791
functions that wrap browser APIs and make them more convenient to use.
Modern web frameworks go a step further, by defining a custom declarative
syntax with which to develop applications. In this model, application code
never directly calls DOM APIs. Instead, application code only describes the
desired state of the UI, and the framework will dynamically generate DOM
API calls to update the UI to match the desired state. Modern web frameworks
are often used in single-page applications, where all page navigation is
replaced with DHTML [26].
When using browser APIs directly or through a simple wrapper, the
amount of script execution required to perform a UI update scales linearly
with the complexity of the update. This is a desirable property for responsive-
ness. When DOM API calls are generated dynamically by a framework, the
framework must perform work to determine exactly which calls are required
for each particular transition, which causes additional overhead.
This presents an interesting problem from a performance point of view:
there are potentially multiple different rendering strategies which can be used
to determine the required API calls, and each strategy might be expected
to have different performance characteristics. In a trivially simple UI, any
strategy is likely to work well enough. It is therefore the scaling of costs that
is of practical concern. Ideally, the cost of an update should depend only on
the complexity of the update, just as when DOM APIs are used directly. A
poorly scaling strategy might in the worst case cause noticeable delays on
even small updates, defeating the purpose of using scripts in the first place.
To our knowledge, there is essentially no previous published research on
the subject of declarative rendering strategies in the web application context.
Previous research on web application performance has focused on the costs
of an initial page load, where costs of resource loading may dominate [35], or
in the performance of the browser’s content rendering process [27]. Scattered
research on JavaScript libraries and frameworks exists, but these typically
do not focus on performance aspects or differentiate between different
types of libraries and frameworks [30], or discuss an older generation of
frameworks [21].
Our contribution to this topic consists of a comparison of the rendering
strategies used in Angular [1], React [8], Vue [15], Svelte [13] and Bla-
zor [2], which are some of the most widely used and influential modern
web frameworks. Using publicly available documentation, we have reviewed
how their rendering strategies work and present a way to estimate their
relative levels of performance. Additionally, we have implemented several
benchmarks where we test expected differences in performance arising from
792 R. Ollila et al.
Figure 1 The critical rendering path used to render web pages in a browser.
2 Background
In this section, we present our findings from a review of Angular, React,
Vue, Svelte and Blazor. We will first briefly explore the motivations behind
using a declarative rendering model, after which we will discuss the rendering
strategies used in these frameworks in detail.
must define every transition and ensure none of them lead to invalid states.
This easily leads to errors such as attempting to manipulate nodes which are
not present in the DOM.
Web application developers have long used libraries and frameworks
to simplify DOM manipulation. Previous generations of frameworks, the
most widely used of which [17] include jQuery [24], MooTools [6] and
Prototype.js [7], provide wrappers over DOM APIs which make them more
convenient to use. They remain imperative, however, and therefore have
fundamentally similar performance characteristics as direct DOM API use,
and remain just as error-prone.
In modern web frameworks, the application developer only needs to
describe the possible states of the application, while the transitions between
states – DOM API calls – are dynamically generated by the framework.
This frees the application developer from the need to manually define state
transitions and potentially entirely eliminates the class of DOM manipulation
errors identified by Ocariza et al. However, the work which a framework must
perform to determine the required API calls for UI transitions represents addi-
tional overhead on top of DOM manipulation. The scaling of this overhead is
the focus of subsequent discussion.
2.2 Frameworks
There exists a huge number of modern web frameworks which implement
a declarative rendering model, and we have not attempted to conduct an
exhaustive survey of them. Instead, we have selected a number of frameworks
which are popular or widely used [11, 12] and show distinct approaches in
their rendering strategies. We have listed the selected frameworks and the
versions used in Table 1. Unless otherwise stated, all information presented
below is sourced from the developer documentation of each framework.
All the selected frameworks are based on JavaScript, with the exception of
Blazor, which is a WebAssembly-based framework. Blazor applications are
written in C# and executed within a .NET runtime that has been compiled to
WebAssembly ahead of time. WebAssembly modules have no direct access
to DOM APIs, and instead must use them through a JavaScript interoper-
ability layer. This is a potential source of overhead which makes Blazor an
interesting comparison with its JavaScript-based peers.
are dirty. Both frameworks achieve this by use of a limited reactivity sys-
tem. In reactive programming, values can be declared as being produced
through computations based on other values. Whenever a value changes,
dependant values are automatically updated transitively by the runtime which
implements the reactive programming model [18].
In the case of Vue and Svelte, a component’s output is treated as part of
a dependency graph, where every value which affects the output is treated
as a dependency of the component. Whenever any such value is mutated or
reassigned, the reactivity system automatically marks the component as dirty
and schedules it to be re-rendered. This works transitively across components
which depend on inputs shared from a parent component, ensuring that all
affected components are marked dirty automatically, and there is no need to
check unaffected components.
Vue’s reactivity system is based on explicitly tracking dependencies at
runtime using proxies, a special type of JavaScript object which enables inter-
cepting access to other objects at runtime [20]. Svelte’s reactivity system, in
contrast, tracks dependency graphs only at compile time, and works by gener-
ating imperative code which at runtime updates dependency graphs whenever
values are reassigned or mutated [23]. Functionally, the two approaches are
equivalent, but Svelte’s approach has potentially lesser runtime costs.
The other aspect of input sizes is the number of elements which must
be processed for each component. A component’s output can be divided
into static and dynamic contents, where static content will never change
after a component’s initial render, whereas dynamic content – content which
depends on data bindings – may change. Of the frameworks we reviewed, we
found that React and Blazor process both static and dynamic content on each
render, whereas all the other frameworks process static content only on the
initial render of a component.
Differences in input sizes determine the scaling in costs of each rendering
strategy. Strategies which are unable to automatically determine dirty com-
ponents scale in cost with the size and complexity of the entire component
tree and therefore suffer from a similar performance profile as page navi-
gation. Rendering strategies which do not optimize the processing of static
content will suffer an additional performance cost factor, the impact of which
depends on the ratio of dynamic versus static content found in the processed
components.
In conclusion, input sizes in update loops present a potential way to
estimate the relative levels of performance of different rendering strategies.
Use of a virtual DOM represents another potential source of overhead due to
798 R. Ollila et al.
3 Benchmarks
This section describes a set of benchmarks we have implemented to measure
performance differences between frameworks. We will first describe the
methodology and aims of the benchmarks, and then present the results.
actions performed. A total of five test scenarios were implemented. Each test
was repeated 10 times, and the results presented are the mean values from 10
samples.
The purpose of these benchmarks is only to measure the relative differ-
ences between frameworks. We present the results measured in absolute terms
as measured in milliseconds, but because of the simplicity of the components
we use, they do not represent the expected performance of a real-world
application with a component tree of equivalent size.
3.2 Results
As outlined in the previous section, we expect the greatest performance
differences to arise when updating content. When creating components and
elements, performance differences should arise purely from fixed costs,
which we have not investigated. These differences can still be measured,
however, which is the purpose of the first two benchmarks.
In the first test scenario, we measure the cost of creating static elements.
The implementation consists of a single component which renders N static
elements within a single component. Table 3 displays the time taken for script
execution on this task.
The second test scenario measures the cost of creating components. In
this case, N components are created in the shape of a binary tree, where each
non-leaf component contains exactly 2 children. The results are shown in
Table 4.
In the remaining test scenarios, we measure the cost of update actions.
The following two scenarios utilize the same component tree, which contains
N components in the shape of a binary tree. The two scenarios differ only
in which component is updated. The results of updating the root compo-
nent of the component tree are shown in Table 5. Table 6 shows the results
Table 4 Script execution time (ms) when creating N components as a binary tree
N Angular React Vue Svelte Blazor
128 20 7 16 3 17
512 75 32 53 10 59
1024 120 55 84 22 128
4096 216 137 223 83 485
8192 297 233 313 142 966
16384 469 394 485 233 1870
32768 774 733 897 482 3644
Table 5 Script execution time (ms) when updating the root component of a component tree
of N components
N Angular React Vue Svelte Blazor
128 3 7 <1 <1 3
512 12 23 <1 <1 3
1024 14 42 <1 <1 2
4096 32 92 <1 <1 3
8192 32 148 <1 <1 3
16384 43 211 <1 <1 2
32768 103 379 <1 <1 3
Table 6 Script execution time (ms) when updating a leaf component in a component tree of
N components
N Angular React Vue Svelte Blazor
128 3 <1 <1 <1 1
512 13 <1 <1 <1 1
1024 14 1 <1 <1 1
4096 33 4 <1 <1 3
8192 33 3 <1 <1 5
16384 44 5 <1 <1 4
32768 104 4 <1 <1 8
Table 7 Script execution time (ms) when updating the entire component tree of N compo-
nents where each component contains primarily static content
N Angular React Vue Svelte Blazor
128 4 34 20 2 28
256 8 44 32 3 60
512 17 66 42 5 101
1024 27 101 72 10 250
2048 29 235 91 20 502
4096 44 289 149 54 1020
8192 238 841 311 80 2013
4 Discussion
In this section, we will discuss the results of the benchmarks. First, we will
discuss the results of each benchmark in turn, and then offer our interpretation
of the validity and applicability of the results.
Figure 2 Script execution time (ms) when rendering N elements in a single component.
Figure 3 Script execution time (ms) when rendering N components as a binary tree.
Table 8 Relative costs of script execution when rendering N components as a binary tree
N Angular React Vue Svelte Blazor
128 6.7 2.3 5.3 1 5.7
512 7.5 3.2 5.3 1 5.9
1024 5.5 2.5 3.8 1 5.8
4096 2.6 1.7 2.7 1 5.8
8192 2.1 1.6 2.2 1 6.8
16384 2.0 1.7 2.1 1 8.0
32768 1.6 1.5 1.9 1 7.5
Table 9 Duration of a full render cycle (ms) when creating a binary tree of N components
N Angular React Vue Svelte Blazor
128 34 14 24 7 23
512 117 45 72 22 74
1024 204 77 115 53 153
4096 556 249 340 198 585
8192 992 464 549 375 1172
16384 1878 858 953 696 1407
32768 3654 1669 1836 1407 4446
than the difference in script execution times alone. This appears to originate
from the fact that Angular generates comment nodes in the DOM when con-
ditional rendering is used (Figure 4), which causes a significant increase in
the size of the DOM in this particular benchmark. While comment nodes
do not contain any meaning for the document’s structure or content, they
increase the size of the DOM node tree and consequently increase the cost
of render tree construction. We found that Blazor exhibits similar behaviour
under other circumstances, but not to an extent which affected the results.
804 R. Ollila et al.
Figure 5 Script execution time (ms) when updating the root component of a component tree
with N components.
Table 10 Duration of a full render cycle (ms) when updating the root component in a tree of
N components
N Angular React Vue Svelte Blazor
128 8 12 4 7 5
512 17 32 9 8 6
1024 26 47 5 4 9
4096 53 105 13 10 20
8192 60 166 20 12 28
16384 98 237 26 22 41
32768 224 439 48 24 51
Figure 6 Script execution times (ms) when updating all components in a component tree of
N components where each component contains primarily static content.
than if only script execution costs are considered, there is still an order of
magnitude of difference in the full render cycle duration between React and
Svelte. Because the changes made to the DOM are very minor, critical path
evaluation is quick, and script execution costs become the dominant factor in
the duration of the render cycle.
When updating static content, we would expect to see a significant advan-
tage for frameworks which only process data bindings on updates, not static
content. This is exactly what we see, with React and Blazor in particular
again having a significant disadvantage due to their inability to differentiate
between static and dynamic content. This difference is very significant,
reaching an order of magnitude as shown in Figure 6 and Table 11. Svelte is
again the best performing framework overall, although the absolute difference
to Angular and Vue is small enough when the number of components is low
that Angular is shown to outperform Svelte when N = 4096.
806 R. Ollila et al.
Table 11 Relative costs of script execution when updating all components in a tree of N
components containing primarily static content
N Angular React Vue Svelte Blazor
128 2.0 17.0 10.0 1 14.0
256 2.7 14.7 10.7 1 20.0
512 3.4 13.2 8.4 1 20.2
1024 2.7 10.1 7.2 1 25.0
2048 1.5 11.8 4.55 1 25.1
4096 0.8 5.4 2.8 1 18.9
8192 3.0 10.5 3.9 1 25.1
Table 12 Duration of a full render cycle (ms) when updating all components in a tree of N
components containing primarily static content
N Angular React Vue Svelte Blazor
128 11 43 29 7 41
256 31 67 56 13 72
512 47 91 67 29 126
1024 72 142 106 56 303
2048 115 295 152 82 605
4096 211 419 270 174 1213
8192 577 1068 542 387 2396
We can again compare script execution times to the duration of the full
render cycle, which is shown in Table 12. Here, layout costs are somewhat
significant due to a large number of changes in the DOM, but costs are still
dominated by script execution. Although the relative costs differences are
smaller, they are still very significant when comparing Blazor and React to
the three other frameworks which process only dynamic content.
5 Conclusions
Page navigation requires the browser to fetch resources and to fully re-render
a web page in order to facilitate transitions in the state of the UI. This
is often an inadequate approach for applications with rich user interfaces
and real-time interaction requirements. Using browser APIs to dynamically
modify the document overcomes these shortcomings, but is itself fraught
with complexity due to the difficulty of managing state transitions in a
complex application. Modern web frameworks solve this problem by pro-
viding a declarative abstraction over rendering which reduces the difficulty
of managing the state of the UI.
Modern Web Frameworks: A Comparison of Rendering Performance 809
The rendering strategies used in modern web frameworks vary not only
in their technical implementation, but in their performance characteristics.
These characteristics may not be obvious to the application developer using
such a framework, yet can have significant consequences for the runtime
performance of any application implemented using it. When choosing a
framework for building a web application, it is therefore crucial to understand
the fundamental characteristics of its rendering strategy.
In our review of Angular, React, Vue, Svelte and Blazor, we found that
there are major differences particularly in the ways they update existing
content. While some frameworks are able to limit an update loop to concern
only those parts of the application which actually need to be updated, others
may need to process unaffected components on each update. Similarly, some
frameworks are able to significantly optimize rendering of static content by
only rendering it once, whereas others must process it on every update.
By benchmarking the frameworks in various situations, we find that
these theoretical differences translate directly into differences in practical
performance, often with an order of magnitude or more of difference between
different strategies. Moreover, we find that there are significant differences
even in fixed costs of creating components and elements. Overall, we find
that significant performance gains are obtained through using a compiler
to optimize rendering of static content, implementing a reactivity system to
accurately track which components need to be updated, and updating the UI
based on individual changes to data bindings rather than explicitly computing
the steps required to update the UI to the desired state.
The modern browser is the most ubiquitous application platform in
existence today. Given the ease of sharing content on the Web and the
browser’s availability on a huge variety of devices, this is unlikely to change
soon. As the use of scripting continues to increase, web application perfor-
mance becomes an increasingly pertinent question particularly on resource
constrained devices. A better understanding of the tools used to produce
content, including their performance characteristics, is therefore a necessity
for ensuring that the Web remains a platform accessible to all.
References
[1] Angular. https://angular.io/. (May 26, 2021).
[2] Blazor |build client web apps with c# |.NET. https://dotnet.microsoft.c
om/apps/aspnet/web-apps/blazor. (May 26, 2021).
810 R. Ollila et al.
Biographies