├── .gitignore ├── .jshintrc ├── README.md ├── benchmark-client ├── browser.json ├── client.js ├── components │ ├── app │ │ ├── component.js │ │ └── index.marko │ └── mount-container │ │ ├── index.marko │ │ └── style.css ├── createRoute.js ├── helpers.js ├── page.marko └── runBenchmark.js ├── benchmark-server ├── collectStats.js ├── generateReports.js ├── reports-csv.js ├── reports-summary.js └── run.js ├── benchmarks.js ├── benchmarks └── search-results │ ├── client.js │ ├── createRoute.js │ ├── marko │ ├── client.js │ ├── components │ │ ├── app-footer │ │ │ └── index.marko │ │ ├── app-search-results-item │ │ │ └── index.marko │ │ └── app │ │ │ └── index.marko │ ├── page.marko │ └── rollup.config.js │ ├── page.marko │ ├── preact │ ├── client.jsx │ ├── components │ │ ├── App.jsx │ │ ├── Footer.jsx │ │ └── SearchResultsItem.jsx │ ├── page.marko │ ├── rollup.config.js │ └── util │ │ ├── htmlSafeJSONStringify.js │ │ └── serverRender.jsx │ ├── react │ ├── client.jsx │ ├── components │ │ ├── App.jsx │ │ ├── Footer.jsx │ │ └── SearchResultsItem.jsx │ ├── page.marko │ ├── rollup.config.js │ └── util │ │ ├── htmlSafeJSONStringify.js │ │ └── reactServerRender.jsx │ └── util │ ├── search-results-data.json │ └── search.js ├── charts ├── cpu.png ├── memory.png ├── requestsPerSecond.png └── responseTime.png ├── generated └── search-results │ ├── charts.xlsx │ ├── marko │ └── html-report │ │ └── index.html │ └── preact │ └── html-report │ └── index.html ├── images ├── test-image-01.jpg ├── test-image-02.jpg ├── test-image-03.jpg ├── test-image-04.jpg ├── test-image-05.jpg └── test-image-06.jpg ├── index.marko ├── init.js ├── package.json ├── scripts ├── bundle.js └── minify.js ├── server.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.marko.js 2 | /node_modules 3 | /build 4 | /.cache 5 | .* -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "Promise" 4 | ], 5 | "node" : true, 6 | "esnext" : true, 7 | "browser" : true, 8 | "boss" : false, 9 | "curly": false, 10 | "debug": false, 11 | "devel": false, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": true, 16 | "laxbreak": false, 17 | "newcap": false, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": true, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": false, 27 | "white": false, 28 | "eqeqeq": false, 29 | "latedef": true, 30 | "unused": "vars", 31 | "strict": false, 32 | "eqnull": true 33 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > __DEPRECATED - Please see the following benchmark that is actively maintained:__ https://github.com/marko-js/isomorphic-ui-benchmarks 2 | 3 | Marko vs React: Performance Benchmark 4 | ======================================== 5 | 6 | _NOTE: This comparison was updated on January 5, 2016 to use `react@15.3.2` and `marko@4.0.0-beta.10`._ 7 | 8 | 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_](http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/) web applications: 9 | 10 | 1. [Marko](https://github.com/marko-js/marko): UI components building library. Marko uses [morphdom](https://github.com/patrick-steele-idem/morphdom) internally to diff/patch the DOM. 11 | 1. [React](http://facebook.github.io/react/): UI components built using JSX and rendered to the DOM using a virtual DOM with a diffing algorithm to minimize updates. UI components must be rendered on the client for behavior to be attached. 12 | 13 | We are interested in the following metrics for comparing the performance of Marko and React: 14 | 15 | - For rendering pages on the server under varying load: 16 | - HTTP response time 17 | - Requests per second 18 | - CPU usage 19 | - Memory usage 20 | - Performance when rendering UI components on the client 21 | - Time needed before the initial page is fully ready on the client 22 | - Page weight (amount of HTML and JS code) 23 | 24 | # Disclaimer 25 | 26 | While I, [Patrick Steele-Idem](https://github.com/patrick-steele-idem), am the author of [Marko](https://github.com/raptorjs/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. 27 | 28 | 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](https://github.com/patrick-steele-idem/morphdom) module to update the DOM with the minimum number of changes 29 | 30 | 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. 31 | 32 | # Test Setup Overview 33 | 34 | 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 `
` with the following information: 35 | 36 | - Image 37 | - Title 38 | - Price 39 | - "Buy Now" button (for testing behavior) 40 | 41 | 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. 42 | 43 | To test server-side rendering performance, the [http-stats](https://www.npmjs.com/package/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. 44 | 45 | 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. 46 | 47 | # Running the Tests 48 | 49 | The following commands should be used to benchmark server-side rendering performance: 50 | 51 | ``` 52 | git clone https://github.com/patrick-steele-idem/marko-vs-react.git 53 | cd marko-vs-react 54 | npm install 55 | npm test 56 | ``` 57 | 58 | _NOTE: The server will automatically be started with `NODE_ENV=production`_ 59 | 60 | The following steps should be used to benchmark client-side rendering performance: 61 | 62 | ``` 63 | NODE_ENV=production node server.js 64 | ``` 65 | 66 | And then launch a web browser and load the following pages: 67 | 68 | - [http://localhost:8080/react](http://localhost:8080/react) 69 | - [http://localhost:8080/marko](http://localhost:8080/marko) 70 | 71 | 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. 72 | 73 | # Results 74 | 75 | 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: 76 | 77 | - MacBook Pro (2.8 GHz Intel Core i7, 16 GB 1600 MHz DDR3) 78 | - Server-side: 79 | - Node.js: v7.3.0 80 | - Client-side: 81 | - (see versions below) 82 | 83 | ## Results Summary 84 | 85 | 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. 86 | 87 | 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. 88 | 89 | 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. 90 | 91 | 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. 92 | 93 | 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). 94 | 95 | 96 | ## Server-side Rendering Performance 97 | 98 | ![Average Response Time](charts/responseTime.png) 99 | 100 | ![Average Requests per Second](charts/requestsPerSecond.png) 101 | 102 | ![Average CPU](charts/cpu.png) 103 | 104 | ![Average Memory](charts/memory.png) 105 | 106 | ## Client-side Rendering Performance 107 | 108 | Time taken to cycle through 100 pages of search results (100 search results items per page): 109 | 110 | ### Chrome 111 | 112 | ``` 113 | marko x 259 ops/sec ±1.22% (59 runs sampled) 114 | react x 220 ops/sec ±1.09% (56 runs sampled) 115 | Fastest is marko 116 | ``` 117 | 118 | _NOTE: Version 55.0.2883.95 (64-bit)_ 119 | 120 | ### Firefox 121 | 122 | ``` 123 | marko x 187 ops/sec ±1.80% (50 runs sampled) 124 | react x 126 ops/sec ±2.15% (52 runs sampled) 125 | Fastest is marko 126 | ``` 127 | 128 | _NOTE: v50.1.0_ 129 | 130 | ### Safari 131 | 132 | ``` 133 | marko x 465 ops/sec ±1.50% (60 runs sampled) 134 | react x 271 ops/sec ±1.69% (53 runs sampled) 135 | Fastest is marko 136 | ``` 137 | 138 | _NOTE: Version 10.0.1 (11602.2.14.0.7)_ 139 | 140 | ### iOS Mobile Safari 141 | 142 | ``` 143 | marko x 108 ops/sec ±2.60% (48 runs sampled) 144 | react x 68.33 ops/sec ±1.65% (47 runs sampled) 145 | Fastest is marko 146 | ``` 147 | 148 | _NOTE: iPhone 6 with iOS 10.2_ 149 | 150 | ## Page Weight 151 | 152 | ### JavaScript Page Weight 153 | 154 | ``` 155 | [marko] 156 | gzip: 11,747 bytes 157 | min: 32,199 bytes 158 | 159 | [react] 160 | gzip: 43,005 bytes 161 | min: 134,459 bytes 162 | ``` 163 | 164 | Source: [marko-js/marko/benchmark/size](https://github.com/marko-js/marko/tree/master/benchmark/size) 165 | 166 | ### HTML Page Weight 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 |
UncompressedGzipped
Marko47.2 KB7.7 KB
React64.3 KB12.1 KB
192 | 193 | The output HTML can be compared using the links below: 194 | 195 | - [Marko output HTML](test/generated/html-marko.html) 196 | - [React output HTML](test/generated/html-react.html) 197 | 198 | # Test Setup Details 199 | 200 | The entry point for the Node.js app that starts the server is [server.js](server.js). The main script creates an Express app with the following routes: 201 | 202 | 1. `/marko` - Renders the search results page using Marko.
Maps to the following controller: [src/marko/pages/search-results/index.js](src/marko/pages/search-results/index.js) 203 | 2. `/react` - Renders the search results page using React.
Maps to the following controller: [src/react/pages/search-results/index.jsx](src/react/pages/search-results/index.jsx) 204 | 3. `/static` - Used to serve up static JS resources 205 | 4. `/` - Serves up the index page 206 | 207 | The code is divided into the following directories: 208 | 209 | - [/src/marko](/src/marko) - All UI components and page code that is specific to Marko 210 | - [/src/react](/src/react) - All UI components and page code that is specific to React 211 | - [/src/shared](/src/shared) - All code that is shared across Marko and React 212 | 213 | For both React and Marko, Marko templates is used to render the page skeleton and the [Lasso.js](https://github.com/lasso-js/lasso) tool is used to deliver all of the required JavaScript code to the browser. 214 | 215 | On both the server and the client, the `NODE_ENV` variable is set to `production` since React behaves differently in non-production mode. 216 | 217 | ## Marko 218 | 219 | For Marko, the [main page template](src/marko/pages/search-results/template.marko) includes a custom tag that delegates rendering of the body of the page to the [``](src/marko/components/app-search-results) UI component: 220 | 221 | ```html 222 | 223 | ``` 224 | 225 | The template for the [``](src/marko/components/app-search-results) component is shown below: 226 | 227 | ```html 228 | 254 |
255 | 258 |
259 | 260 | 263 |
264 | 265 |
266 | ``` 267 | 268 | The template for the [``](src/marko/components/app-search-results-item) component is shown below: 269 | 270 | ```html 271 | 278 | 279 | var itemData=data.itemData 280 | 281 |
282 |

${itemData.title}

283 | itemData.title/ 284 | ${itemData.price} 285 | 286 |
287 | ``` 288 | 289 | 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. 290 | 291 | ## React 292 | 293 | For React, code similar to the following is used to produce the body HTML for the page: 294 | 295 | ```javascript 296 | var React = require('react'); 297 | var SearchResults = require('src/react/components/SearchResults'); 298 | var searchResultsHTML = React.renderToString( 299 | ); 300 | ``` 301 | 302 | The `render` method for the [`SearchResults`](src/react/components/SearchResults.jsx) UI component contains the following code: 303 | 304 | ```javascript 305 | function render() { 306 | var searchResultsData = this.state.searchResultsData; 307 | 308 | return ( 309 |
310 | 313 |
314 | {searchResultsData.items.map(function(item) { 315 | return 316 | })} 317 |
318 |
319 | ); 320 | } 321 | ``` 322 | 323 | 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 ` 6 | 7 | 8 |

${data.libName}

9 |
10 |
11 |
12 |
13 |
-------------------------------------------------------------------------------- /benchmark-client/components/mount-container/style.css: -------------------------------------------------------------------------------- 1 | .mount-container { 2 | height: 600px; 3 | width: 300px; 4 | overflow: scroll; 5 | display: inline-block; 6 | } -------------------------------------------------------------------------------- /benchmark-client/createRoute.js: -------------------------------------------------------------------------------- 1 | var template = require('./page.marko'); 2 | 3 | var isProduction = process.env.NODE_ENV === 'production'; 4 | 5 | function createRoute(benchmark, routeOptions) { 6 | var bundles = []; 7 | benchmark.benches.forEach((bench) => { 8 | // if (bench.name !== 'marko') { 9 | // return; 10 | // } 11 | bundles.push(`/build/${benchmark.name}/bundles${isProduction ? '.min' : ''}/${bench.name}.js`); 12 | }); 13 | 14 | return function(req, res) { 15 | res.marko(template, { 16 | $global: { 17 | benchmark 18 | }, 19 | bundles 20 | }); 21 | }; 22 | } 23 | 24 | module.exports = createRoute; 25 | -------------------------------------------------------------------------------- /benchmark-client/helpers.js: -------------------------------------------------------------------------------- 1 | var mountContainer = require('./components/mount-container'); 2 | 3 | var mountEls = {}; 4 | 5 | function createMountEl(libName) { 6 | var key = libName; 7 | var mountWidget = mountContainer.renderSync({ 8 | libName: libName 9 | }) 10 | .appendTo(document.getElementById('mount')) 11 | .getWidget(); 12 | 13 | mountEls[key] = mountWidget.el; 14 | 15 | return mountWidget.getEl('output'); 16 | } 17 | 18 | function showSingleMountEl(libName) { 19 | var key = libName; 20 | 21 | for (var curKey in mountEls) { 22 | var mountEl = mountEls[curKey]; 23 | if (curKey === key) { 24 | mountEl.style.display = 'inline-block'; 25 | } else { 26 | mountEl.style.display = 'none'; 27 | } 28 | } 29 | } 30 | 31 | function showMountEl(libName) { 32 | var key = libName; 33 | 34 | var mountEl = mountEls[key]; 35 | mountEl.style.display = 'inline-block'; 36 | } 37 | 38 | exports.createMountEl = createMountEl; 39 | exports.showSingleMountEl = showSingleMountEl; 40 | exports.showMountEl = showMountEl; -------------------------------------------------------------------------------- /benchmark-client/page.marko: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | 5 | 6 | 7 | 8 | 9 | ${out.global.benchmark.name} | Marko Benchmark 10 | 11 | 12 | 13 |

${out.global.benchmark.name} | Marko Benchmark

14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /benchmark-client/runBenchmark.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | 3 | function addListeners(eventEmitter, config) { 4 | Object.keys(config).forEach(function(name) { 5 | if (name.startsWith('on')) { 6 | var listener = config[name]; 7 | var eventName = name.charAt(2).toLowerCase() + name.substring(3); 8 | eventEmitter.on(eventName, listener); 9 | } 10 | }); 11 | } 12 | 13 | function delay(durationMillis) { 14 | return new Promise(function(resolve, reject) { 15 | setTimeout(resolve, durationMillis); 16 | }); 17 | } 18 | 19 | module.exports = function runBenchmark(name, options) { 20 | 21 | var Suite = window.Benchmark.Suite; 22 | var suite = new Suite(name); 23 | 24 | 25 | var benches = []; 26 | 27 | var suiteEvents = new EventEmitter(); 28 | var userSetup; 29 | 30 | function load() { 31 | return Promise.resolve() 32 | .then(function() { 33 | if (typeof options === 'function') { 34 | return options(); 35 | } else { 36 | return options; 37 | } 38 | }) 39 | .then(function(options) { 40 | addListeners(suiteEvents, options); 41 | 42 | var benchPromiseChain = Promise.resolve(); 43 | 44 | Object.keys(options.benches).forEach(function(name) { 45 | benchPromiseChain = benchPromiseChain.then(function() { 46 | var bench = options.benches[name]; 47 | if (typeof bench === 'function') { 48 | return bench(name); 49 | } else { 50 | return bench; 51 | } 52 | }) 53 | .then(function(bench) { 54 | var benchFn = bench.fn; 55 | userSetup = bench.setup; 56 | 57 | var benchEvents = new EventEmitter(); 58 | 59 | addListeners(benchEvents, bench); 60 | 61 | var actualFn; 62 | 63 | var defer = benchFn.length === 1; 64 | 65 | if (defer) { 66 | actualFn = function(deferred) { 67 | function done() { 68 | deferred.resolve(); 69 | } 70 | 71 | benchFn(done); 72 | }; 73 | } else { 74 | actualFn = function(deferred) { 75 | benchFn(); 76 | }; 77 | } 78 | 79 | bench = Object.assign({name: name}, bench); 80 | bench.events = benchEvents; 81 | 82 | suite.add(name, { 83 | // a flag to indicate the benchmark is deferred 84 | defer: defer, 85 | // benchmark test function 86 | fn: actualFn, 87 | 88 | onStart: function() { 89 | benchEvents.emit('start'); 90 | suiteEvents.emit('startBench', bench); 91 | } 92 | }); 93 | 94 | benches.push(bench); 95 | }); 96 | }); 97 | 98 | return benchPromiseChain; 99 | }); 100 | } 101 | 102 | function setup() { 103 | var promiseChain = Promise.resolve(); 104 | 105 | if (userSetup) { 106 | promiseChain = promiseChain.then(userSetup); 107 | } 108 | 109 | benches.forEach(function(bench) { 110 | if (bench.setup) { 111 | promiseChain = promiseChain.then(function() { 112 | bench.setup(); 113 | }); 114 | } 115 | }); 116 | 117 | return promiseChain; 118 | } 119 | 120 | function warmupCycle() { 121 | var promiseChain = Promise.resolve(); 122 | benches.forEach(function(bench) { 123 | var benchFn = bench.fn; 124 | promiseChain = promiseChain.then(function() { 125 | if (benchFn.length === 1) { 126 | return new Promise(function(resolve, reject) { 127 | benchFn(resolve); 128 | }); 129 | } else { 130 | benchFn(); 131 | } 132 | }); 133 | }); 134 | 135 | return promiseChain; 136 | } 137 | 138 | function warmup() { 139 | suiteEvents.emit('warmup'); 140 | 141 | benches.forEach(function(bench) { 142 | bench.events.emit('warmup'); 143 | }); 144 | 145 | var index = 0; 146 | var totalCount = 100; 147 | 148 | function next() { 149 | return warmupCycle() 150 | .then(function() { 151 | if (++index === totalCount) { 152 | suiteEvents.emit('warmupComplete'); 153 | return delay(1000); 154 | } else { 155 | return delay(10).then(next); 156 | } 157 | }); 158 | } 159 | 160 | return next(); 161 | } 162 | 163 | function run() { 164 | return new Promise(function(resolve, reject) { 165 | suite 166 | .on('start', function(event) { 167 | suiteEvents.emit('start', { 168 | suite: suite 169 | }); 170 | }) 171 | .on('cycle', function(event) { 172 | suiteEvents.emit('cycle', { 173 | suite: suite, 174 | resultsString: String(event.target) 175 | }); 176 | }) 177 | .on('complete', function() { 178 | suiteEvents.emit('complete', { 179 | suite: suite, 180 | resultsString: 'Fastest is ' + this.filter('fastest').map('name') + '\n\n--------------\n' 181 | }); 182 | 183 | suite.off('start cycle complete'); 184 | resolve(); 185 | }) 186 | .on('error', function(e) { 187 | suite.off('start cycle complete error'); 188 | reject(e.target.error); 189 | }) 190 | .run({ 'async': true }); 191 | }); 192 | } 193 | 194 | return { 195 | on: function(eventName, listener) { 196 | suiteEvents.on(eventName, listener); 197 | return this; 198 | }, 199 | 200 | run: function() { 201 | return Promise.resolve() 202 | .then(load) 203 | .then(setup) 204 | .then(warmup) 205 | .then(run); 206 | } 207 | }; 208 | }; -------------------------------------------------------------------------------- /benchmark-server/collectStats.js: -------------------------------------------------------------------------------- 1 | var httpStats = require('http-stats'); 2 | var child_process = require('child_process'); 3 | var http = require('http'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var serverScriptPath = path.join(__dirname, '../server.js'); 7 | var rootOutputDir = path.join(__dirname, '../generated'); 8 | var mkdirp = require('mkdirp'); 9 | 10 | function cleanupStats(stats) { 11 | var steps = stats.steps; 12 | 13 | for (var i=0; i { 64 | serverProcess.on('close', function(code) { 65 | isServerRunning = false; 66 | process.removeListener('exit', onProcessExit); 67 | 68 | console.log('Server process exited with code ' + code + '\n'); 69 | 70 | if (code !== 0) { 71 | return reject(new Error('FAILURE: Server process exited with code ' + code)); 72 | } 73 | 74 | serverProcess = null; 75 | resolve(); 76 | }); 77 | }); 78 | 79 | killServerProcess = function killServerProcess() { 80 | if (serverProcess) { 81 | serverProcess.kill(); 82 | serverProcess = null; 83 | } 84 | 85 | return killServerProcessPromise; 86 | }; 87 | 88 | return new Promise((resolve, reject) => { 89 | serverProcess.on('message', function(message) { 90 | if (message === 'online') { 91 | isServerRunning = true; 92 | resolve(); 93 | } 94 | }); 95 | }); 96 | }) 97 | .then(function saveResponseHTML() { 98 | return new Promise((resolve, reject) => { 99 | http.get(url, function(res) { 100 | var outputFilePath = path.join(outputDir, 'output.html'); 101 | res 102 | .pipe(fs.createWriteStream(outputFilePath, 'utf8')) 103 | .on('error', function(err) { 104 | reject(err); 105 | }) 106 | .on('finish', function() { 107 | if (res.statusCode !== 200) { 108 | console.log('Output file: ' + outputFilePath); 109 | throw new Error('Status code: ' + res.statusCode + ' - Response not OK: ' + url); 110 | } 111 | 112 | resolve(); 113 | }); 114 | }); 115 | }); 116 | }) 117 | .then(function startMeasuring() { 118 | return new Promise((resolve, reject) => { 119 | console.log('Collecting stats for ' + url + ' (pid: ' + serverProcess.pid + ')...'); 120 | 121 | httpStats.measure({ 122 | url: url, 123 | beginConcurrency: 1, 124 | endConcurrency: 5, 125 | stepRequests: 500, 126 | pids: [serverProcess.pid], 127 | report: { 128 | outputDir: path.join(outputDir, 'html-report') 129 | } 130 | }, 131 | function done(err, httpStats) { 132 | if (err) { 133 | return reject(err); 134 | } 135 | finalStats = cleanupStats(httpStats); 136 | console.log('Completed stats collection for ' + url + ' (pid: ' + serverProcess.pid + ').\nKilling server process...'); 137 | resolve(); 138 | }); 139 | }); 140 | }) 141 | .then(function stopServer() { 142 | if (!isServerRunning) { 143 | throw new Error(`Server is not running for ${benchmark.name}/${bench.name}`); 144 | } 145 | 146 | return killServerProcess(); 147 | }) 148 | .then(() => { 149 | return finalStats; 150 | }); 151 | } 152 | 153 | module.exports = collectStats; -------------------------------------------------------------------------------- /benchmark-server/generateReports.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var mkdirp = require('mkdirp'); 3 | 4 | module.exports = function generateReports(data, options) { 5 | var promiseChain = Promise.resolve(); 6 | var outputDir = options.outputDir; 7 | 8 | Object.keys(data).forEach((benchmarkName) => { 9 | var reportOptions = { 10 | outputDir: path.join(outputDir, benchmarkName) 11 | }; 12 | 13 | try { 14 | mkdirp.sync(reportOptions.outputDir); 15 | } catch(e) {} 16 | 17 | var benchmarkStats = data[benchmarkName]; 18 | console.log(benchmarkName); 19 | console.log('=============================='); 20 | promiseChain = promiseChain 21 | .then(() => { 22 | return require('./reports-summary').generate(benchmarkStats, reportOptions); 23 | }) 24 | .then(() => { 25 | return require('./reports-csv').generate(benchmarkStats, reportOptions); 26 | }); 27 | }); 28 | 29 | return promiseChain; 30 | }; -------------------------------------------------------------------------------- /benchmark-server/reports-csv.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var access = require('safe-access'); 4 | 5 | function CSVWriter() { 6 | this.lines = []; 7 | this.currentRow = null; 8 | } 9 | 10 | CSVWriter.prototype = { 11 | writeRow: function(values) { 12 | this.lines.push(values.join(',')); 13 | }, 14 | 15 | beginRow: function() { 16 | this.currentRow = []; 17 | }, 18 | 19 | writeCol: function(value) { 20 | this.currentRow.push(value); 21 | }, 22 | 23 | endRow: function() { 24 | this.writeRow(this.currentRow); 25 | this.currentRow = null; 26 | }, 27 | 28 | writeFile: function(filename) { 29 | var output = this.lines.join('\n'); 30 | fs.writeFileSync(filename, output, 'utf8'); 31 | } 32 | }; 33 | 34 | exports.generate = function generate(results, options) { 35 | var outputDir = options.outputDir; 36 | var names = Object.keys(results); 37 | var first = results[names[0]]; 38 | var stepCount = first.steps.length; 39 | 40 | function writeCSVFile(filename, prop) { 41 | var csvWriter = new CSVWriter(); 42 | 43 | csvWriter.writeRow(['Concurrency'].concat(names)); 44 | 45 | for (var stepIndex=0; stepIndex max) { 84 | max = value; 85 | maxName = name; 86 | } 87 | 88 | if (value < min) { 89 | min = value; 90 | minName = name; 91 | } 92 | } 93 | values[name] = value; 94 | }); 95 | 96 | 97 | writeLine(label + ':'); 98 | writeLine(); 99 | 100 | names.forEach(function(name) { 101 | var value = values[name]; 102 | var output = ' ' + leftPad(name, maxNameLength) + ': ' + leftPad(format(value, unit), 13); 103 | var suffix = ''; 104 | var percentageDifference; 105 | 106 | if (comparison === 'lowerBetter') { 107 | if (minName === name) { 108 | suffix = 'winner'; 109 | } else { 110 | percentageDifference = (value - min) / min * 100; 111 | suffix += percentageDifference.toFixed(2) + '% worse'; 112 | } 113 | } else if (comparison === 'higherBetter') { 114 | if (maxName === name) { 115 | suffix = 'winner'; 116 | } else { 117 | percentageDifference = (max - value) / max * 100; 118 | suffix += percentageDifference.toFixed(2) + '% worse'; 119 | } 120 | } 121 | 122 | if (suffix) { 123 | output += ' (' + suffix + ')'; 124 | } 125 | 126 | writeLine(output); 127 | }); 128 | 129 | writeLine(); 130 | } 131 | 132 | for (var stepIndex=0; stepIndex { 18 | promiseChain = promiseChain.then(() => { 19 | return collectStats(benchmark, bench) 20 | .then((stats) => { 21 | statsByLibName[bench.name] = stats; 22 | }); 23 | }); 24 | }); 25 | 26 | return promiseChain.then(() => { 27 | return statsByLibName; 28 | }); 29 | } 30 | 31 | function run() { 32 | try { 33 | fs.mkdirSync(outputDir); 34 | } catch(e) {} 35 | 36 | var stats = {}; 37 | 38 | var promiseChain = Promise.resolve(); 39 | benchmarks.forEach((benchmark) => { 40 | promiseChain = promiseChain.then(() => { 41 | return runBenchmark(benchmark) 42 | .then((benchmarkStats) => { 43 | stats[benchmark.name] = benchmarkStats; 44 | }); 45 | }); 46 | }); 47 | 48 | return promiseChain.then(() => { 49 | return stats; 50 | }); 51 | } 52 | 53 | run() 54 | .then((stats) => { 55 | console.log('Stats collection successfully completed!'); 56 | 57 | var json = JSON.stringify(stats, null, 2); 58 | fs.writeFileSync(path.join(outputDir, 'data.json'), json, 'utf8'); 59 | 60 | return generateReports( 61 | stats, 62 | { 63 | outputDir: outputDir 64 | }); 65 | }) 66 | .catch((err) => { 67 | console.error('Stats collection failed. Error:', err.stack || err); 68 | process.exit(1); 69 | }); -------------------------------------------------------------------------------- /benchmarks.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | var benchmarksDir = path.join(__dirname, 'benchmarks'); 5 | 6 | var benchmarks = []; 7 | 8 | fs.readdirSync(benchmarksDir).forEach((benchmarkName) => { 9 | var benchmarkDir = path.join(benchmarksDir, benchmarkName); 10 | 11 | if (!fs.statSync(benchmarkDir).isDirectory()) { 12 | // Only look at directories 13 | return; 14 | } 15 | 16 | var benchmark = { 17 | name: benchmarkName, 18 | dir: benchmarkDir, 19 | benches: [], 20 | createRoute: require(path.join(benchmarkDir, 'createRoute')) 21 | }; 22 | 23 | benchmarks.push(benchmark); 24 | 25 | fs.readdirSync(benchmarkDir).forEach((libName) => { 26 | if (libName === 'util') { 27 | return; 28 | } 29 | 30 | var libDir = path.join(benchmarkDir, libName); 31 | if (!fs.statSync(libDir).isDirectory()) { 32 | // Only look at directories 33 | return; 34 | } 35 | var bench = { 36 | dir: libDir, 37 | name: libName, 38 | url: `/${benchmarkName}/${libName}`, 39 | 40 | }; 41 | 42 | benchmark.benches.push(bench); 43 | }); 44 | }); 45 | 46 | module.exports = benchmarks; -------------------------------------------------------------------------------- /benchmarks/search-results/client.js: -------------------------------------------------------------------------------- 1 | var searchService = require('./util/search'); 2 | 3 | window.registerBenchmark(function(helpers) { 4 | return { 5 | createBench: function(libName, factoryFunc) { 6 | var mountEl = helpers.createMountEl(libName); 7 | var pageIndex = 0; 8 | 9 | function getNextSearchResults() { 10 | return searchService.performSearch({ pageIndex: pageIndex++ }); 11 | } 12 | 13 | var fn = factoryFunc(mountEl, getNextSearchResults); 14 | 15 | return { 16 | onWarmup: function() { 17 | pageIndex = 0; 18 | helpers.showMountEl(libName); 19 | }, 20 | onStart: function() { 21 | pageIndex = 0; 22 | helpers.showSingleMountEl(libName); 23 | }, 24 | fn 25 | }; 26 | } 27 | }; 28 | }); -------------------------------------------------------------------------------- /benchmarks/search-results/createRoute.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var searchService = require('./util/search'); 4 | var pageLayoutTemplate = require('./page.marko'); 5 | 6 | module.exports = function createRoute(libName, options) { 7 | var pageTemplate = require(`./${libName}/page.marko`); 8 | 9 | return function(req, res) { 10 | res.setHeader('Content-Type', 'text/html; charset=utf-8'); 11 | 12 | pageTemplate.render({ 13 | $global: { 14 | jsBundle: options.jsBundle, 15 | title: libName 16 | }, 17 | pageLayout: pageLayoutTemplate, 18 | searchResults: searchService.performSearch({}) 19 | }, res); 20 | 21 | res.on('error', function(err) { 22 | console.error('ERROR:', err); 23 | }); 24 | }; 25 | }; -------------------------------------------------------------------------------- /benchmarks/search-results/marko/client.js: -------------------------------------------------------------------------------- 1 | var app = require('./components/app'); 2 | require('marko/widgets').initWidgets(); 3 | 4 | window.addBench('marko', function(el, getNextSearchResults) { 5 | 6 | var widget = app.renderSync({ 7 | searchResultsData: getNextSearchResults() 8 | }) 9 | .appendTo(el) 10 | .getWidget(); 11 | 12 | return function(done) { 13 | widget.setProps({ 14 | searchResultsData: getNextSearchResults() 15 | }); 16 | 17 | widget.update(); 18 | done(); 19 | }; 20 | }); -------------------------------------------------------------------------------- /benchmarks/search-results/marko/components/app-footer/index.marko: -------------------------------------------------------------------------------- 1 | 511 | -------------------------------------------------------------------------------- /benchmarks/search-results/marko/components/app-search-results-item/index.marko: -------------------------------------------------------------------------------- 1 | class { 2 | onInput(input) { 3 | this.state = { 4 | purchased: false, 5 | item: input.item 6 | }; 7 | } 8 | 9 | handleBuyButtonClick() { 10 | this.state.purchased = true; 11 | } 12 | } 13 | 14 | var item=state.item 15 | 16 |
17 |

${item.title}

18 | 19 |
20 |
21 | 22 | item.title/ 23 | 24 |
25 |
26 | 27 | ${item.price} 28 | 29 | Purchased!
30 | 31 | 32 | 33 | Buy now! 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /benchmarks/search-results/marko/components/app/index.marko: -------------------------------------------------------------------------------- 1 | class { 2 | onMount() { 3 | window.onMount(); 4 | } 5 | } 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /benchmarks/search-results/marko/page.marko: -------------------------------------------------------------------------------- 1 | 2 | <@body> 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /benchmarks/search-results/marko/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjsPlugin from 'rollup-plugin-commonjs'; 2 | import browserifyPlugin from 'rollup-plugin-browserify-transform'; 3 | import nodeResolvePlugin from 'rollup-plugin-node-resolve'; 4 | import markoify from 'markoify'; 5 | import envify from 'envify'; 6 | import minpropsify from 'minprops/browserify'; 7 | import path from 'path'; 8 | 9 | export default { 10 | entry: path.join(__dirname, 'client.js'), 11 | format: 'iife', 12 | moduleName: 'app', 13 | plugins: [ 14 | browserifyPlugin(markoify), 15 | browserifyPlugin(envify), 16 | browserifyPlugin(minpropsify), 17 | nodeResolvePlugin({ 18 | jsnext: true, // Default: false 19 | main: true, // Default: true 20 | browser: true, // Default: false 21 | preferBuiltins: false, 22 | extensions: [ '.js', '.marko' ] 23 | }), 24 | commonjsPlugin({ 25 | include: [ 'node_modules/**', '**/*.marko', '**/*.js'], 26 | extensions: [ '.js', '.marko' ] 27 | }) 28 | ], 29 | dest: path.join(process.env.BUNDLES_DIR, 'marko.js') 30 | }; -------------------------------------------------------------------------------- /benchmarks/search-results/page.marko: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | Marko vs React: ${out.global.title} 13 | 14 | 15 |

Marko vs React: ${out.global.title}

16 |