DEPRECATED - Please see the following benchmark that is actively maintained: https://github.com/marko-js/isomorphic-ui-benchmarks
NOTE: This comparison was updated on January 5, 2016 to use react@15.3.2
and marko@4.0.0-beta.10
.
The goal of this project is to provide a real-world sample app that can be used to compare the performance of two different approaches of building UI component-centric, isomorphic web applications:
We are interested in the following metrics for comparing the performance of Marko and React:
While I, Patrick Steele-Idem, am the author of Marko, every effort was made to be fair and unbiased when putting together this comparison. I've also asked other developers to review this benchmark. If you find a problem please open a Github issue to discuss.
Both Marko and React have a lot of merit when it comes to building webapps based on UI components, and I was genuinely curious to see how they performed in practice. Both React and Marko a DOM diffing/patching algorithm to update the view. However, React makes use of a virtual DOM while Marko only utilizes the real DOM. Marko uses the morphdom module to update the DOM with the minimum number of changes
Also, this benchmark focuses on both server-side and client-side rendering performance. Many apps benefit from rendering the initial page on the server since it supports SEO and it often reduces the time to render the initial page. Both React and Marko support rendering on both the server and in the browser.
To compare performance, both React and Marko were used to build a page that displays search results consisting of 100 search results items using eBay listings as sample data. Each search results item is rendered into a <div>
with the following information:
The initial page of search results is rendered on the server and the app allows additional pages of search results to be rendered completely on the client.
To test server-side rendering performance, the http-stats tool is used to put the server under increasing load while collecting statistics. A freshly started server is used to test Marko and React independently.
To test client-side rendering performance, a simple script is used to cycle through 100 pages of sample search results data (100 search items per page). After each page is rendered and after the resulted are flushed to the DOM, the next page of search results is rendered until the test completes. The total time used to complete the test is used to measure the performance of client-side rendering.
The following commands should be used to benchmark server-side rendering performance:
git clone https://github.com/patrick-steele-idem/marko-vs-react.git
cd marko-vs-react
npm install
npm test
_NOTE: The server will automatically be started with NODE_ENV=production
_
The following steps should be used to benchmark client-side rendering performance:
NODE_ENV=production node server.js
And then launch a web browser and load the following pages:
On each page, click on the button at the top labeled "Start Client Performance Tests". The results of the test will be shown at the top of the page.
The test setup is described in more detail later in this document, but below are the results of running the tests with the following configuration:
On the server, Marko was the winner by far. On the client, React and Marko performed nearly the same. However, on an iPhone 6 running iOS 8.4, Marko performed significantly better.
On the server, Marko was able to render the page much more quickly and with much lower CPU and memory usage. These benchmarks indicate that React needs to go through a lot more optimization before it is ready to be used on the server.
It is also worth pointing out that React also suffers from a severe disadvantage in that a page rendered on the server must be re-rendered on the client in order for behavior to be bound to UI components. When the re-rendering happens on the client, the exact same input data needs to be provided to the top-level UI component. For this benchmark, this means that the data used to render the initial pages of search results needed to be serialized to JSON and included in the output HTML rendered on the server. Even while temporarily disabling the serialization of JSON it was still observed that React was significantly slower doing server-side rendering.
On the client, Marko and React were able to cycle through pages of search results in about the same time. The performance gap between Marko and React will likely vary by use case and will be sensitive to the DOM structure.
Finally, a web page that uses React will have a significantly higher weight due to the size of the React JavaScript library, as well as the addition of React-specific data-*
attributes (e.g., data-reactid=".t9c80npc00.1.$1.1"
) that are added to the output HTML during rendering. The page weight also increased due to the embedding of the extra JSON required to re-render the initial page on the client (discussed earlier).
Time taken to cycle through 100 pages of search results (100 search results items per page):
marko x 259 ops/sec ±1.22% (59 runs sampled)
react x 220 ops/sec ±1.09% (56 runs sampled)
Fastest is marko
NOTE: Version 55.0.2883.95 (64-bit)
marko x 187 ops/sec ±1.80% (50 runs sampled)
react x 126 ops/sec ±2.15% (52 runs sampled)
Fastest is marko
NOTE: v50.1.0
marko x 465 ops/sec ±1.50% (60 runs sampled)
react x 271 ops/sec ±1.69% (53 runs sampled)
Fastest is marko
NOTE: Version 10.0.1 (11602.2.14.0.7)
marko x 108 ops/sec ±2.60% (48 runs sampled)
react x 68.33 ops/sec ±1.65% (47 runs sampled)
Fastest is marko
NOTE: iPhone 6 with iOS 10.2
[marko]
gzip: 11,747 bytes
min: 32,199 bytes
[react]
gzip: 43,005 bytes
min: 134,459 bytes
Source: marko-js/marko/benchmark/size
Uncompressed | Gzipped | ||
Marko | 47.2 KB | 7.7 KB | ✔ |
React | 64.3 KB | 12.1 KB | ✖ |
The output HTML can be compared using the links below:
The entry point for the Node.js app that starts the server is server.js. The main script creates an Express app with the following routes:
/marko
- Renders the search results page using Marko./react
- Renders the search results page using React./static
- Used to serve up static JS resources/
- Serves up the index pageThe code is divided into the following directories:
For both React and Marko, Marko templates is used to render the page skeleton and the Lasso.js tool is used to deliver all of the required JavaScript code to the browser.
On both the server and the client, the NODE_ENV
variable is set to production
since React behaves differently in non-production mode.
For Marko, the main page template includes a custom tag that delegates rendering of the body of the page to the <app-search-results>
UI component:
<app-search-results search-results-data=searchResultsData/>
The template for the <app-search-results>
component is shown below:
<script>
var measurePageCycling = require('~/src/shared/util/measurePageCycling');
module.exports = {
getTemplateData: function(state, input) {
return input.searchResultsData;
},
handleStartButtonClick: function() {
var self = this;
var searchResultsData;
// Start cycling through search result pages and update the DOM
// after receiving each search results page
measurePageCycling(
function updateDOM(newSearchResultsData, done) {
searchResultsData = newSearchResultsData;
self.setProps({
searchResultsData: searchResultsData
});
self.update();
done();
});
}
};
</script>
<div class="search-results">
<button type="button" onClick("handleStartButtonClick")>
Start Client Performance Tests
</button>
<div>
<!-- Loop over the search results items: -->
<app-search-results-item item-data=item
for(i, item in data.items)
ref="item-${i}"/>
</div>
<app-footer/>
</div>
The template for the <app-search-results-tem>
component is shown below:
<script>
module.exports = {
handleBuyButtonClick: function() {
this.el.style.backgroundColor = 'yellow';
}
};
</script>
var itemData=data.itemData
<div class="search-results-item">
<h2>${itemData.title}</h2>
<img src=itemData.image alt=itemData.title/>
<span>${itemData.price}</span>
<button type="button" onClick("handleBuyButtonClick")>Buy now!</button>
</div>
During rendering of the Marko templates, Marko keeps track of all the rendered widgets (including the HTML element that they are bound to). This information is passed to the client as part of the DOM so that the widgets can efficiently be initialized when the page loads. If a UI component is rendered on the client, the list of rendered widgets is kept in memory and after the resulting HTML is added to the DOM all of the rendered widgets are then initialized.
For React, code similar to the following is used to produce the body HTML for the page:
var React = require('react');
var SearchResults = require('src/react/components/SearchResults');
var searchResultsHTML = React.renderToString(
<SearchResults searchResultsData={searchResultsData}/>);
The render
method for the SearchResults
UI component contains the following code:
function render() {
var searchResultsData = this.state.searchResultsData;
return (
<div className="search-results">
<button type="button" onClick={this.handleStartButtonClick}>
Start Client Performance Tests
</button>
<div>
{searchResultsData.items.map(function(item) {
return <SearchResultsItem key={item.id} itemData={item}/>
})}
</div>
</div>
);
}
For behavior to be attached on the client, the page must be re-rendered on the client using the same search results data. Therefore, the React testbed also includes additional code to serialize the search results data to JSON. The resulting JSON data is dropped into the page inside a <script>
block that introduces a new global variable.
The re-rendering of the initial page on the client is done in src/react/pages/search-results/client.jsx using the following code:
var React = require('react');
var ReactDOM = require('react-dom');
var SearchResults = require('src/react/components/SearchResults');
var mountNode = document.getElementById("searchResultsMount");
ReactDOM.render(
<SearchResults searchResultsData={window.searchResultsData}/>,
mountNode);
Related code:
It's important to emphasize that this benchmark is only comparing one specific use case: rendering a page of search results. The numbers gathered here suggest that there is significant overhead when using React on the server. On the client, React has the performance advantage at runtime but the gap is much smaller. However, React requires a much large amount of JavaScript to be downloaded and executed.
If you are interested in the raw data, much of the data can be found in the following directory:
If you see any problems or have any suggestions please let us know. Every effort was made to be as fair and accurate as possible, but mistakes do happen. If you find a problem please open a Github issue to discuss.