├── .github
└── workflows
│ └── benchmark.yml
├── .gitignore
├── README.md
├── app.js
├── assets
├── src
│ ├── app
│ │ ├── banner
│ │ │ ├── index.hyperapp.js
│ │ │ ├── index.inferno.js
│ │ │ ├── index.marko
│ │ │ ├── index.preact.js
│ │ │ ├── index.rax.js
│ │ │ ├── index.react.js
│ │ │ ├── index.vue.js
│ │ │ └── styles.js
│ │ ├── index.hyperapp.js
│ │ ├── index.inferno.js
│ │ ├── index.marko
│ │ ├── index.preact.js
│ │ ├── index.rax.js
│ │ ├── index.react.js
│ │ ├── index.vue.js
│ │ ├── index.xtpl
│ │ └── list
│ │ │ ├── index.hyperapp.js
│ │ │ ├── index.inferno.js
│ │ │ ├── index.marko
│ │ │ ├── index.preact.js
│ │ │ ├── index.rax.js
│ │ │ ├── index.react.js
│ │ │ ├── index.vue.js
│ │ │ └── styles.js
│ ├── client.hyperapp.js
│ ├── client.inferno.js
│ ├── client.marko.js
│ ├── client.preact.js
│ ├── client.rax.js
│ ├── client.react.js
│ ├── client.vue.js
│ ├── server.hyperapp.js
│ ├── server.inferno.js
│ ├── server.preact.js
│ ├── server.rax.js
│ ├── server.react.js
│ └── server.vue.js
└── static
│ └── index.css
├── benchmarks
├── ab-request.sh
├── index.sh
└── renderToString.js
├── controllers
├── hyperapp.js
├── inferno.js
├── marko.js
├── preact.js
├── rax.js
├── react.js
├── vue.js
└── xtpl.js
├── marko.json
├── mock
├── banner.js
└── list.js
├── package.json
├── views
├── page.marko
└── page.xtpl
├── webpack.client.config.js
└── webpack.server.config.js
/.github/workflows/benchmark.yml:
--------------------------------------------------------------------------------
1 | name: Benchmark
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - 'bench/**'
8 |
9 | jobs:
10 | build-and-publish:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v1
14 | - uses: actions/setup-node@v1
15 | with:
16 | node-version: 10
17 | registry-url: https://registry.npmjs.org/
18 | - run: npm i
19 | - run: npm run webpack
20 | - run: npm run benchmark
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .travis.yml
3 | *.swp
4 | .sass-cache/
5 | .DS_Store
6 | .idea/
7 | zip/
8 | *.tmp
9 | *.bak
10 | tmp/
11 | temp/
12 | coverage/
13 | npm-debug.log*
14 | assets/build
15 |
16 | .happypack
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Server-side Rendering Comparison
2 |
3 | ## Benchmark info
4 |
5 | - With `NODE_ENV=production`. `renderToString` both require from `lib` not `dist`.
6 | - With about 600 dom nodes.
7 |
8 | ## Run
9 |
10 | ```bash
11 | # prepare
12 | npm install
13 | npm run webpack
14 |
15 | # run renderToString benchmark
16 | npm run benchmark
17 | ```
18 |
19 | ## Result
20 |
21 | > https://github.com/raxjs/server-side-rendering-comparison/commit/04fdfec8aa8626312ad3e2c0c6a57c6f735ad6de/checks?check_suite_id=401266841
22 |
23 | ```bash
24 | React(16.12.0)#renderToString x 1,178 ops/sec ±1.23% (85 runs sampled)
25 | Rax(1.1.1)#renderToString x 6,047 ops/sec ±1.73% (82 runs sampled)
26 | Inferno(7.3.3)#renderToString x 3,335 ops/sec ±1.77% (82 runs sampled)
27 | Preact(10.2.1)#renderToString x 1,005 ops/sec ±1.10% (86 runs sampled)
28 | Marko(4.18.33)#renderToString x 10,291 ops/sec ±1.64% (86 runs sampled)
29 | xtemplate(4.7.2)#render x 20,600 ops/sec ±2.89% (84 runs sampled)
30 |
31 | The benchmark was run on:
32 | PLATFORM: linux 5.0.0-1027-azure
33 | CPU: Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz
34 | SYSTEM MEMORY: 6.782737731933594GB
35 | NODE VERSION: v10.18.1
36 | ```
37 |
38 | - Result run by [Github Actions](https://github.com/raxjs/server-side-rendering-comparison/actions)
39 |
40 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | process.env.NODE_ENV = 'production';
4 |
5 | const fs = require('fs');
6 | const koa = require('koa');
7 | const serve = require('koa-static');
8 | const router = require('koa-router')();
9 |
10 | const hyperappController = require('./controllers/hyperapp');
11 | const reactController = require('./controllers/react');
12 | const raxController = require('./controllers/rax');
13 | const vueController = require('./controllers/vue');
14 | const preactController = require('./controllers/preact');
15 | const markoController = require('./controllers/marko');
16 | const infernoController = require('./controllers/inferno');
17 | const xtplController = require('./controllers/xtpl');
18 |
19 | const app = require('xtpl/lib/koa')(require('koa')(), {
20 | views:'./views'
21 | });
22 |
23 | router.get('/hyperapp', hyperappController.home);
24 | router.get('/react', reactController.home);
25 | router.get('/rax', raxController.home);
26 | router.get('/vue', vueController.home);
27 | router.get('/preact', preactController.home);
28 | router.get('/marko', markoController.home);
29 | router.get('/inferno', infernoController.home);
30 | router.get('/xtpl', xtplController.home);
31 |
32 | app.use(serve('./assets/build'));
33 | app.use(serve('./assets/static'));
34 | app.use(router.routes());
35 |
36 | app.listen(3300, () => {
37 | console.log('Server start listen at 3300');
38 | });
39 |
--------------------------------------------------------------------------------
/assets/src/app/banner/index.hyperapp.js:
--------------------------------------------------------------------------------
1 | /** @jsx h */
2 | import { h } from "hyperapp";
3 |
4 | export const init = [];
5 |
6 | const alertFx = (dispatch, { message }) => alert(message);
7 | const alertEffect = message => [alertFx, { message }];
8 |
9 | const ClickAction = (state, item) => [state, alertEffect("click banner:" + item.title)];
10 |
11 | export const view = state => (
12 |
13 |
Hyperapp Banner:
14 |
15 | {state.map((item, idx) => (
16 |
17 |

18 |
19 | ))}
20 |
21 |
22 | );
23 |
--------------------------------------------------------------------------------
/assets/src/app/banner/index.inferno.js:
--------------------------------------------------------------------------------
1 | import {Component} from 'inferno';
2 |
3 | export default class Banner extends Component {
4 |
5 | state = {
6 | data: this.props.data || []
7 | };
8 |
9 | onBannerClick = (item) => {
10 | alert('click banner:' + item.title);
11 | }
12 |
13 | render() {
14 |
15 | const {data} = this.state;
16 |
17 | return (
18 |
19 |
Inferno Banner:
20 |
21 | {
22 | data.map((item, idx) => {
23 | return (
24 |
25 |

26 |
27 | );
28 | })
29 | }
30 |
31 |
32 | );
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/assets/src/app/banner/index.marko:
--------------------------------------------------------------------------------
1 | class {
2 | onCreate() {
3 | this.state = {
4 | title: 'Marko Banner'
5 | };
6 | }
7 | onBannerClick(title) {
8 | alert('click banner:' + title);
9 | }
10 | }
11 |
12 |
13 |
${state.title}
14 |
15 |
16 |
17 |

18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/assets/src/app/banner/index.preact.js:
--------------------------------------------------------------------------------
1 | /** @jsx h */
2 | 'use strict';
3 |
4 | import { h, render, Component } from 'preact';
5 |
6 | export default class Banner extends Component {
7 |
8 | state = {
9 | data: this.props.data || []
10 | };
11 |
12 | onBannerClick = (item) => {
13 | alert('click banner:' + item.title);
14 | }
15 |
16 | render() {
17 |
18 | const {data} = this.state;
19 |
20 | return (
21 |
22 |
Preact Banner:
23 |
24 | {
25 | data.map((item, idx) => {
26 | return (
27 |
28 |

29 |
30 | );
31 | })
32 | }
33 |
34 |
35 | );
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/assets/src/app/banner/index.rax.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 |
3 | "use strict";
4 |
5 | import { createElement, Component } from "rax";
6 |
7 | export default class Banner extends Component {
8 | state = {
9 | data: this.props.data || []
10 | };
11 |
12 | onBannerClick = item => {
13 | alert("click banner:" + item.title);
14 | };
15 |
16 | render() {
17 | const { data } = this.state;
18 | return (
19 |
20 |
Rax Banner:
21 |
22 | {data.map((item, idx) => {
23 | return (
24 |
28 |

29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/assets/src/app/banner/index.react.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 |
5 | export default class Banner extends React.Component {
6 |
7 | state = {
8 | data: this.props.data || []
9 | };
10 |
11 | onBannerClick = (item) => {
12 | alert('click banner:' + item.title);
13 | }
14 |
15 | render() {
16 | const {data} = this.state;
17 |
18 | return (
19 |
20 |
React Banner:
21 |
22 | {
23 | data.map((item, idx) => {
24 | return (
25 |
26 |

27 |
28 | );
29 | })
30 | }
31 |
32 |
33 | );
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/assets/src/app/banner/index.vue.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | props: ['data'],
4 |
5 | render(h) {
6 |
7 | return (
8 |
9 |
Vue Banner:
10 |
11 | {
12 | this.data.map((item, idx) => {
13 | return (
14 |
15 |

16 |
17 | );
18 | })
19 | }
20 |
21 |
22 | )
23 | },
24 |
25 | methods: {
26 | onBannerClick: (item) => {
27 | alert('click banner:' + item.title);
28 | }
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/assets/src/app/banner/styles.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | export default {
4 | container: {
5 | width: '900px',
6 | margin: '30px auto',
7 | border: '1px solid #eee',
8 | padding: '20px'
9 | },
10 | list: {
11 | overflow: 'hidden'
12 | },
13 | item: {
14 | float: 'left',
15 | width: '175px',
16 | marginRight: '20px'
17 | },
18 | itemImg: {
19 | width: '100%',
20 | height: '100px',
21 | marginBottom: '10px'
22 | }
23 | }
--------------------------------------------------------------------------------
/assets/src/app/index.hyperapp.js:
--------------------------------------------------------------------------------
1 |
2 | /** @jsx h */
3 | import { h } from 'hyperapp';
4 | import * as Banner from './banner/index.hyperapp';
5 | import * as List from './list/index.hyperapp';
6 |
7 | export const init = {
8 | bannerData: Banner.init,
9 | listData: List.init,
10 | };
11 |
12 | export const view = (state) => (
13 |
14 | {Banner.view(state.bannerData)}
15 | {List.view(state.listData)}
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/assets/src/app/index.inferno.js:
--------------------------------------------------------------------------------
1 | import {Component} from 'inferno';
2 | import List from './list/index.inferno';
3 | import Banner from './banner/index.inferno';
4 |
5 | export default class App extends Component {
6 |
7 | componentDidMount() {
8 | // console.log('inferno render in client');
9 | }
10 |
11 | render() {
12 | const props = this.props || {};
13 |
14 | return (
15 |
16 |
17 |
18 |
19 | );
20 |
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/assets/src/app/index.marko:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/assets/src/app/index.preact.js:
--------------------------------------------------------------------------------
1 |
2 | /** @jsx h */
3 |
4 | import { h, render, Component } from 'preact';
5 | import List from './list/index.preact';
6 | import Banner from './banner/index.preact';
7 |
8 | export default class App extends Component {
9 |
10 | componentDidMount() {
11 | // console.log('react render in client');
12 | }
13 |
14 | render() {
15 |
16 | const props = this.props || {};
17 |
18 | return (
19 |
20 |
21 |
22 |
23 | );
24 |
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/assets/src/app/index.rax.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 |
3 | import { createElement, Component } from 'rax';
4 | import List from './list/index.rax';
5 | import Banner from './banner/index.rax';
6 |
7 | export default class App extends Component {
8 |
9 | componentDidMount() {
10 | // console.log('rax render in client');
11 | }
12 |
13 | render() {
14 | const props = this.props || {};
15 |
16 | return (
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | };
25 |
--------------------------------------------------------------------------------
/assets/src/app/index.react.js:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import List from './list/index.react';
4 | import Banner from './banner/index.react';
5 |
6 | export default class App extends React.Component {
7 | state = {};
8 |
9 | componentDidMount() {
10 | // console.log('react render in client');
11 | this.setState({
12 | onClick: function(){
13 |
14 | }
15 | });
16 | }
17 |
18 | render() {
19 |
20 | const props = this.props || {};
21 |
22 | return (
23 |
24 |
25 |
26 |
27 | );
28 |
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/assets/src/app/index.vue.js:
--------------------------------------------------------------------------------
1 |
2 | import List from './list/index.vue';
3 | import Banner from './banner/index.vue';
4 |
5 | export default {
6 | props: ['bannerData', 'listData'],
7 | render(h) {
8 |
9 | return (
10 |
11 |
12 |
13 |
14 | )
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/assets/src/app/index.xtpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Xtpl Banner:
5 |
6 | {{#each(bannerData)}}
7 |
8 |

9 |
10 | {{/each}}
11 |
12 |
13 |
14 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/assets/src/app/list/index.hyperapp.js:
--------------------------------------------------------------------------------
1 | /** @jsx h */
2 | import { h } from "hyperapp";
3 |
4 | export const init = [];
5 |
6 | export const view = state => (
7 |
8 |
HyperappList
9 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/assets/src/app/list/index.inferno.js:
--------------------------------------------------------------------------------
1 | import {Component} from 'inferno';
2 |
3 | export default class List extends Component {
4 |
5 | state = {
6 | data: this.props.data || []
7 | };
8 |
9 | render() {
10 | const { data } = this.state;
11 | return (
12 |
13 |
InfernoList
14 |
29 |
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/assets/src/app/list/index.marko:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/src/app/list/index.preact.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /** @jsx h */
3 |
4 | import { h, render, Component } from 'preact';
5 |
6 | export default class List extends Component {
7 |
8 | state = {
9 | data: this.props.data || []
10 | };
11 |
12 | render() {
13 | const { data } = this.state;
14 | return (
15 |
16 |
PreactList
17 |
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/assets/src/app/list/index.rax.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 |
3 | 'use strict';
4 |
5 | import { createElement, Component } from 'rax';
6 |
7 | export default class List extends Component {
8 |
9 | state = {
10 | data: this.props.data || []
11 | };
12 |
13 | render() {
14 | const { data } = this.state;
15 |
16 | return (
17 |
35 | );
36 | }
37 | }
--------------------------------------------------------------------------------
/assets/src/app/list/index.react.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React, { Component } from 'react';
4 |
5 | export default class List extends Component {
6 |
7 | state = {
8 | data: this.props.data || []
9 | };
10 |
11 | render() {
12 | const { data } = this.state;
13 | return (
14 |
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/assets/src/app/list/index.vue.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 |
3 | 'use strict';
4 |
5 | export default {
6 |
7 | props: ['data'],
8 |
9 | render(h) {
10 | return (
11 |
29 | );
30 | }
31 | }
--------------------------------------------------------------------------------
/assets/src/app/list/styles.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | export default {
4 | container: {
5 | width: '900px',
6 | margin: '30px auto',
7 | border: '1px solid #eee',
8 | padding: '20px'
9 | },
10 | list: {
11 | overflow: 'hidden'
12 | },
13 | item: {
14 | float: 'left',
15 | width: '175px',
16 | marginRight: '20px'
17 | },
18 | itemImg: {
19 | width: '100%',
20 | height: '100px'
21 | },
22 | itemTitle: {
23 | height: '30px',
24 | lineHeight: '30px'
25 | },
26 | itemPrice: {
27 | height: '30px',
28 | lineHeight: '30px'
29 | }
30 | }
--------------------------------------------------------------------------------
/assets/src/client.hyperapp.js:
--------------------------------------------------------------------------------
1 | import { app } from 'hyperapp';
2 |
3 | import { init, view } from './app/index.hyperapp';
4 |
5 | app({ init, view, node: document.getElementById('container') });
6 |
--------------------------------------------------------------------------------
/assets/src/client.inferno.js:
--------------------------------------------------------------------------------
1 |
2 | import { hydrate } from 'inferno-hydrate';
3 |
4 | import App from './app/index.inferno';
5 |
6 | hydrate(, document.getElementById('container'));
7 |
--------------------------------------------------------------------------------
/assets/src/client.marko.js:
--------------------------------------------------------------------------------
1 | import MyTemplate from "./app/index.marko";
2 |
3 | MyTemplate.renderSync({...window.GLOBAL}).replaceChildrenOf(document.getElementById('container'));
--------------------------------------------------------------------------------
/assets/src/client.preact.js:
--------------------------------------------------------------------------------
1 | /** @jsx h */
2 | import { h, render, Component } from 'preact';
3 |
4 | import App from './app/index.preact';
5 |
6 | render(, document.getElementById('container'));
--------------------------------------------------------------------------------
/assets/src/client.rax.js:
--------------------------------------------------------------------------------
1 | /** @jsx createElement */
2 |
3 | 'use strict';
4 |
5 | import { createElement, Component, render, hydrate } from 'rax';
6 | import * as driverDom from 'driver-dom';
7 |
8 | import App from './app/index.rax';
9 |
10 | render(, document.getElementById('container'), { driver: driverDom, hydrate: true});
11 |
--------------------------------------------------------------------------------
/assets/src/client.react.js:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import ReactDom from 'react-dom';
4 |
5 | import App from './app/index.react';
6 |
7 | ReactDom.hydrate(, document.getElementById('container'));
8 |
--------------------------------------------------------------------------------
/assets/src/client.vue.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | import App from './app/index.vue';
4 |
5 | new Vue({
6 | el: '#container',
7 | render (h) {
8 | return (
9 |
10 | )
11 | }
12 | })
--------------------------------------------------------------------------------
/assets/src/server.hyperapp.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./app/index.hyperapp');
2 |
--------------------------------------------------------------------------------
/assets/src/server.inferno.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./app/index.inferno');
2 |
--------------------------------------------------------------------------------
/assets/src/server.preact.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./app/index.preact');
--------------------------------------------------------------------------------
/assets/src/server.rax.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./app/index.rax');
--------------------------------------------------------------------------------
/assets/src/server.react.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./app/index.react');
--------------------------------------------------------------------------------
/assets/src/server.vue.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./app/index.vue');
--------------------------------------------------------------------------------
/assets/static/index.css:
--------------------------------------------------------------------------------
1 | .container {
2 | font-size: 19px;
3 | width: 900px;
4 | margin: 30px auto;
5 | border: 1px solid #eee;
6 | padding: 20px;
7 | }
8 |
9 | .list {
10 | overflow: hidden;
11 | }
12 |
13 | .item {
14 | float: left;
15 | width: 175px;
16 | margin-right: 20px;
17 | }
18 |
19 | .itemImg {
20 | width: 100%;
21 | height: 100px;
22 | margin-bottom: 10px
23 | }
24 |
25 | .itemTitle {
26 | height: 30px;
27 | line-height: 30px;
28 | }
29 |
30 | .itemPrice {
31 | height: '30px';
32 | line-height: '30px';
33 | }
--------------------------------------------------------------------------------
/benchmarks/ab-request.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # compare req&res by ab cmd
4 |
5 | # before: npm run start
6 |
7 | ab -n1000 -c50 http://127.0.0.1:3300/rax
8 | ab -n1000 -c50 http://127.0.0.1:3300/react
9 | ab -n1000 -c50 http://127.0.0.1:3300/inferno
10 | ab -n1000 -c50 http://127.0.0.1:3300/vue
--------------------------------------------------------------------------------
/benchmarks/index.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | export NODE_ENV=production
4 | cd benchmarks/
5 |
6 | echo '-----------compare renderToString----------'
7 | node ./renderToString.js
--------------------------------------------------------------------------------
/benchmarks/renderToString.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * compare renderToString
5 | */
6 | const os = require('os');
7 | const fs = require('fs');
8 | const path = require('path');
9 | const Benchmark = require('benchmark');
10 | const xtemplate = require('xtemplate');
11 | const Rax = require('rax');
12 | const raxRenderToString = require('rax-server-renderer').renderToString;
13 | const React = require('react');
14 | const ReactDOMServer = require('react-dom/server');
15 | const Vue = require('vue');
16 | const vueRenderToString = require('vue-server-renderer').createRenderer().renderToString;
17 | const Preact = require('preact');
18 | const preactRenderToString = require('preact-render-to-string');
19 | const InfernoServer = require('inferno-server');
20 | const infernoCreateElement = require('inferno-create-element');
21 | const Hyperapp = require('hyperapp-render');
22 |
23 | const hyperappPkg = require('hyperapp/package.json');
24 | const reactPkg = require('react/package.json');
25 | const raxPkg = require('rax/package.json');
26 | const infernoPkg = require('inferno/package.json');
27 | const preactPkg = require('preact/package.json');
28 | const vuePkg = require('vue/package.json');
29 | const markoPkg = require('marko/package.json');
30 | const xtemplatePkg = require('xtemplate/package.json');
31 |
32 | const HyperApp = require('../assets/build/server.hyperapp.bundle');
33 | const ReactApp = require('../assets/build/server.react.bundle').default;
34 | const RaxApp = require('../assets/build/server.rax.bundle').default;
35 | const VueApp = require('../assets/build/server.vue.bundle').default;
36 | const PreactApp = require('../assets/build/server.preact.bundle').default;
37 | const MarkoApp = require('../assets/build/server.marko.bundle');
38 | const InfernoApp = require('../assets/build/server.inferno.bundle').default;
39 |
40 | const xtplPath = path.join(__dirname, '../assets/src/app/index.xtpl');
41 | const xtplString = fs.readFileSync(xtplPath, 'utf8');
42 | const xtpl = xtemplate.compile(xtplString);
43 |
44 | const data = {
45 | listData: require('../mock/list'),
46 | bannerData: require('../mock/banner')
47 | };
48 |
49 | const suite = new Benchmark.Suite;
50 |
51 | suite
52 | .add(`Hyperapp(${hyperappPkg.version})#renderToString`, function() {
53 | Hyperapp.renderToString(HyperApp.view(data));
54 | })
55 | .add(`React(${reactPkg.version})#renderToString`, function() {
56 | ReactDOMServer.renderToString(React.createElement(ReactApp, data));
57 | })
58 | .add(`Rax(${raxPkg.version})#renderToString`, function() {
59 | raxRenderToString(Rax.createElement(RaxApp, data));
60 | })
61 | .add(`Inferno(${infernoPkg.version})#renderToString`, function() {
62 | InfernoServer.renderToString(infernoCreateElement.createElement(InfernoApp, data));
63 | })
64 | .add(`Preact(${preactPkg.version})#renderToString`, function() {
65 | preactRenderToString(Preact.h(PreactApp, data));
66 | })
67 | .add(`Vue(${vuePkg.version})#renderToString`, function(deferred) {
68 | const vueVm = new Vue({
69 | render(h) {
70 | return h(VueApp, {
71 | attrs: {
72 | listData: data.listData,
73 | bannerData: data.bannerData
74 | }
75 | });
76 | }
77 | });
78 | // Do not call deferred.resolve in the callback of renderToString, it will bring more delay time
79 | vueRenderToString(vueVm).then(html => {
80 | deferred.resolve();
81 | });
82 | }, {defer: true})
83 | .add(`Marko(${markoPkg.version})#renderToString`, function() {
84 | MarkoApp.renderToString(data);
85 | })
86 | .add(`xtemplate(${xtemplatePkg.version})#render`, function(){
87 | const xtplApp = new xtemplate(xtpl);
88 | xtplApp.render(data);
89 | })
90 | .on('cycle', function(event) {
91 | console.log(String(event.target));
92 | // const t = event.target.stats.mean;
93 | // console.log('mean:' + (t*1000000).toFixed(6) + 'μs');
94 | })
95 | .on('complete', function() {
96 | console.log();
97 | console.log('The benchmark was run on:');
98 | const osInformation = getOSInformation();
99 | Object.keys(osInformation).map(info => {
100 | console.log(' ' + info.toUpperCase() + ': ' + osInformation[info]);
101 | });
102 | })
103 | // run async
104 | .run({ 'async': true });
105 |
106 | function getOSInformation() {
107 | return {
108 | platform: os.platform() + ' ' + os.release(),
109 | cpu: os.cpus()[0].model,
110 | 'system memory': os.totalmem() / ( 1024 * 1024 * 1024 ) + 'GB',
111 | 'node version': process.version
112 | };
113 | }
--------------------------------------------------------------------------------
/controllers/hyperapp.js:
--------------------------------------------------------------------------------
1 | const { renderToString } = require("hyperapp-render");
2 |
3 | module.exports = {
4 | home: function*() {
5 | const Hyperapp = require("../assets/build/server.hyperapp.bundle").default;
6 | const pageConfig = {
7 | listData: require("../mock/list"),
8 | bannerData: require("../mock/banner")
9 | };
10 | yield this.render("page", {
11 | type: "hyperapp",
12 | content: renderToString(Hyperapp.view, Hyperapp.init),
13 | global: JSON.stringify(pageConfig)
14 | });
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/controllers/inferno.js:
--------------------------------------------------------------------------------
1 | const {createElement} = require('inferno-create-element');
2 | const {renderToString} = require('inferno-server');
3 |
4 | module.exports = {
5 |
6 | home: function* () {
7 | const InfernoApp = require('../assets/build/server.inferno.bundle').default;
8 |
9 | const pageConfig = {
10 | listData: require('../mock/list'),
11 | bannerData: require('../mock/banner')
12 | };
13 |
14 | renderToString(createElement(InfernoApp, pageConfig))
15 |
16 | yield this.render('page', {
17 | type: 'inferno',
18 | content: renderToString(
19 | createElement(InfernoApp, pageConfig)
20 | ),
21 | global: JSON.stringify(pageConfig)
22 | });
23 |
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/controllers/marko.js:
--------------------------------------------------------------------------------
1 |
2 | const pageTemplatePath = require.resolve('../views/page.marko');
3 | const pageTemplate = require('marko').load(pageTemplatePath, { writeToDisk:false });
4 |
5 | module.exports = {
6 |
7 | home: function* () {
8 |
9 | const app = require('../assets/build/server.marko.bundle');
10 | const pageConfig = {
11 | listData: require('../mock/list'),
12 | bannerData: require('../mock/banner')
13 | };
14 |
15 | this.body = pageTemplate.renderToString({ pageConfig, app, globalData: JSON.stringify(pageConfig) });
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/controllers/preact.js:
--------------------------------------------------------------------------------
1 |
2 | const preact = require('preact');
3 | const preactRenderToString = require('preact-render-to-string');
4 |
5 | module.exports = {
6 |
7 | home: function* () {
8 |
9 | const PreactApp = require('../assets/build/server.preact.bundle').default;
10 | const pageConfig = {
11 | listData: require('../mock/list'),
12 | bannerData: require('../mock/banner')
13 | };
14 |
15 | yield this.render('page', {
16 | type: 'preact',
17 | content: preactRenderToString(preact.h(PreactApp, pageConfig)),
18 | global: JSON.stringify(pageConfig)
19 | });
20 |
21 | }
22 |
23 | }
--------------------------------------------------------------------------------
/controllers/rax.js:
--------------------------------------------------------------------------------
1 |
2 | const Rax = require('rax');
3 | const raxRenderToString = require('rax-server-renderer').renderToString;
4 |
5 | module.exports = {
6 |
7 | home: function* () {
8 |
9 | const RaxApp = require('../assets/build/server.rax.bundle').default;
10 | const pageConfig = {
11 | listData: require('../mock/list'),
12 | bannerData: require('../mock/banner')
13 | };
14 |
15 | const element = Rax.createElement(RaxApp, pageConfig);
16 | const content = raxRenderToString(element);
17 |
18 | yield this.render('page', {
19 | type: 'rax',
20 | content,
21 | global: JSON.stringify(pageConfig)
22 | });
23 |
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/controllers/react.js:
--------------------------------------------------------------------------------
1 |
2 | const React = require('react');
3 | const ReactDOMServer = require('react-dom/server');
4 |
5 | module.exports = {
6 |
7 | home: function* () {
8 |
9 | const ReactApp = require('../assets/build/server.react.bundle').default;
10 |
11 | const pageConfig = {
12 | listData: require('../mock/list'),
13 | bannerData: require('../mock/banner')
14 | };
15 |
16 | const element = React.createElement(ReactApp, pageConfig);
17 | const content = ReactDOMServer.renderToString(element);
18 |
19 | yield this.render('page', {
20 | type: 'react',
21 | content,
22 | global: JSON.stringify(pageConfig)
23 | });
24 |
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/controllers/vue.js:
--------------------------------------------------------------------------------
1 |
2 | const Vue = require('vue');
3 | const vueRenderToString = require('vue-server-renderer').createRenderer().renderToString;
4 |
5 | module.exports = {
6 |
7 | home: function* () {
8 |
9 | const VueApp = require('../assets/build/server.vue.bundle').default;
10 | const pageConfig = {
11 | listData: require('../mock/list'),
12 | bannerData: require('../mock/banner')
13 | };
14 |
15 | const vm = new Vue({
16 | render(h) {
17 | return h(VueApp, {
18 | attrs: {
19 | listData: pageConfig.listData,
20 | bannerData: pageConfig.bannerData
21 | }
22 | });
23 | }
24 | });
25 |
26 | const content = yield new Promise((resolve, reject) => {
27 | vueRenderToString(vm, (err, html) => {
28 | if(err) {
29 | return reject(err);
30 | }
31 | resolve(html);
32 | });
33 | });
34 |
35 | yield this.render('page', {
36 | type: 'vue',
37 | content: content,
38 | global: JSON.stringify(pageConfig)
39 | });
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/controllers/xtpl.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 | const xtemplate = require('xtemplate');
4 |
5 | module.exports = {
6 |
7 | home: function* () {
8 | const pageConfig = {
9 | listData: require('../mock/list'),
10 | bannerData: require('../mock/banner')
11 | };
12 |
13 | const xtplPath = path.join(__dirname, '../assets/src/app/index.xtpl');
14 | const xtplString = fs.readFileSync(xtplPath, 'utf8');
15 | const xtpl = xtemplate.compile(xtplString);
16 | const xtplApp = new xtemplate(xtpl);
17 |
18 | const content = xtplApp.render(pageConfig);
19 |
20 | yield this.render('page', {
21 | type: 'xtpl',
22 | content: content,
23 | global: JSON.stringify(pageConfig)
24 | });
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/marko.json:
--------------------------------------------------------------------------------
1 | {
2 | "tags-dir":"./assets/src/app"
3 | }
4 |
--------------------------------------------------------------------------------
/mock/banner.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = Array.from({ length: 100})
4 | .map((undef, i) => {
5 | return {
6 | title: 'banner ' + i,
7 | img: 'https://img.alicdn.com/tps/TB13keMLXXXXXbmXVXXXXXXXXXX-900-500.jpg'
8 | };
9 | });
--------------------------------------------------------------------------------
/mock/list.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = Array.from({ length: 100})
4 | .map((undef, i) => {
5 | return {
6 | title: 'Product title ' + i,
7 | img: 'https://img.alicdn.com/tps/TB13keMLXXXXXbmXVXXXXXXXXXX-900-500.jpg',
8 | href: 'https://github.com',
9 | price: 20
10 | };
11 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server-side-rendering-comparison",
3 | "version": "0.1.0",
4 | "description": "Server-side rendering comparison",
5 | "main": "app.js",
6 | "scripts": {
7 | "start": "NODE_ENV=production nodemon app.js",
8 | "webpack:client": "webpack --config webpack.client.config.js",
9 | "webpack:server": "webpack --config webpack.server.config.js",
10 | "webpack": "npm run webpack:client && npm run webpack:server",
11 | "benchmark": "NODE_ENV=production bash ./benchmarks/index.sh"
12 | },
13 | "keywords": [
14 | "ssr"
15 | ],
16 | "repository": {
17 | "type": "git",
18 | "url": "https://github.com/taobaofed/server-side-rendering-comparison"
19 | },
20 | "publishConfig": {
21 | "registry": "http://registry.npm.alibaba-inc.com"
22 | },
23 | "author": {
24 | "name": "imsobear"
25 | },
26 | "dependencies": {
27 | "driver-dom": "^2.0.0",
28 | "hyperapp": "^2.0.3",
29 | "hyperapp-render": "^3.1.0",
30 | "inferno": "^7.2.1",
31 | "inferno-create-element": "^7.3.0",
32 | "inferno-hydrate": "^7.3.1",
33 | "inferno-server": "^7.2.1",
34 | "koa": "^1.2.4",
35 | "koa-router": "^5.4.0",
36 | "koa-static": "^2.0.0",
37 | "marko": "latest",
38 | "preact": "latest",
39 | "preact-render-to-string": "latest",
40 | "rapscallion": "^2.1.5",
41 | "rax": "latest",
42 | "rax-server-renderer": "latest",
43 | "react": "latest",
44 | "react-dom": "latest",
45 | "vue": "latest",
46 | "vue-server-renderer": "latest",
47 | "xtemplate": "^4.6.0",
48 | "xtpl": "^3.3.0"
49 | },
50 | "devDependencies": {
51 | "@babel/core": "^7.5.5",
52 | "@babel/plugin-proposal-class-properties": "^7.5.5",
53 | "@babel/plugin-proposal-decorators": "^7.4.4",
54 | "@babel/plugin-transform-react-constant-elements": "^7.5.0",
55 | "@babel/plugin-transform-react-jsx": "^7.3.0",
56 | "@babel/plugin-transform-runtime": "^7.5.5",
57 | "@babel/preset-env": "^7.5.5",
58 | "@babel/preset-react": "^7.0.0",
59 | "@babel/preset-typescript": "^7.3.3",
60 | "babel-helper-vue-jsx-merge-props": "^2.0.3",
61 | "babel-loader": "^8.0.6",
62 | "babel-plugin-inferno": "^6.1.0",
63 | "babel-plugin-syntax-jsx": "^6.18.0",
64 | "babel-plugin-transform-jsx-to-html": "^0.1.0",
65 | "babel-plugin-transform-vue-jsx": "^3.7.0",
66 | "benchmark": "^2.1.3",
67 | "marko-loader": "^1.2.0",
68 | "nodemon": "^1.11.0",
69 | "uglifyjs-webpack-plugin": "^2.1.3",
70 | "webpack": "^4.38.0",
71 | "webpack-cli": "^3.3.6",
72 | "webpack-dev-server": "^3.7.2"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/views/page.marko:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Marko Page
6 |
7 |
10 |
11 |
12 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/views/page.xtpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{type}} Page
6 |
7 |
8 |
9 |
12 |
22 |
23 |
24 |
25 | {{{ content }}}
26 |
27 | {{#if(type === 'react')}}
28 |
29 |
30 | {{elseif (type === 'rax')}}
31 |
32 | {{elseif (type === 'inferno')}}
33 |
34 | {{elseif (type === 'vue')}}
35 |
36 | {{elseif (type === 'preact')}}
37 |
38 | {{/if}}
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/webpack.client.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var fs = require('fs');
3 | var webpack = require('webpack');
4 |
5 | module.exports = {
6 | mode: 'production',
7 | entry: {
8 | 'client.hyperapp': './assets/src/client.hyperapp.js',
9 | 'client.marko': './assets/src/client.marko.js',
10 | 'client.react': './assets/src/client.react.js',
11 | 'client.rax': './assets/src/client.rax.js',
12 | 'client.preact': './assets/src/client.preact.js',
13 | 'client.vue': './assets/src/client.vue.js',
14 | 'client.inferno': './assets/src/client.inferno.js'
15 | },
16 | output: {
17 | path: path.join(process.cwd(), 'assets/build'),
18 | filename: '[name].bundle.js',
19 | },
20 | module: {
21 | rules:[
22 | {
23 | test: /\.hyperapp\.js?$/,
24 | loader: 'babel-loader',
25 | options: {
26 | presets: [
27 | [require.resolve('@babel/preset-env'), {
28 | targets: {
29 | browsers: ['last 2 versions', 'IE >= 9']
30 | },
31 | modules: false,
32 | loose: true
33 | }],
34 | ],
35 | plugins: [
36 | [require.resolve('@babel/plugin-transform-react-jsx'), {pragma: 'h'}],
37 | ]
38 | }
39 | },
40 | // react & rax
41 | {
42 | test: /(rax|react|.)\.jsx?$/,
43 | loader: 'babel-loader',
44 | options: {
45 | presets: [
46 | [require.resolve('@babel/preset-env'), {
47 | targets: {
48 | browsers: ['last 2 versions', 'IE >= 9']
49 | },
50 | modules: false,
51 | loose: true
52 | }],
53 | [require.resolve('@babel/preset-react')],
54 | ],
55 | plugins: [
56 | [require.resolve('@babel/plugin-transform-runtime')],
57 | [require.resolve('@babel/plugin-transform-react-jsx')],
58 | [require.resolve('@babel/plugin-proposal-decorators'), {legacy: true}],
59 | [require.resolve('@babel/plugin-proposal-class-properties'), {loose: true}]
60 | ]
61 | }
62 | },
63 | {
64 | test: /\.preact\.js?$/,
65 | loader: 'babel-loader',
66 | options: {
67 | presets: [
68 | [require.resolve('@babel/preset-env'), {
69 | targets: {
70 | browsers: ['last 2 versions', 'IE >= 9']
71 | },
72 | modules: false,
73 | loose: true
74 | }],
75 | [require.resolve('@babel/preset-react')],
76 | ],
77 | plugins: [
78 | [require.resolve('@babel/plugin-transform-runtime')],
79 | [require.resolve('@babel/plugin-transform-react-jsx'), {pragma: 'h'}],
80 | [require.resolve('@babel/plugin-proposal-decorators'), {legacy: true}],
81 | [require.resolve('@babel/plugin-proposal-class-properties'), {loose: true}],
82 | ]
83 | }
84 | },
85 | {
86 | test: /\.inferno\.js?$/,
87 | loader: 'babel-loader',
88 | options: {
89 | presets: [
90 | [require.resolve('@babel/preset-env'), {
91 | modules: false,
92 | loose: true
93 | }],
94 | ],
95 | plugins: [
96 | [require.resolve("babel-plugin-inferno"), {imports: true}],
97 | [require.resolve('@babel/plugin-transform-runtime')],
98 | [require.resolve('@babel/plugin-proposal-decorators'), {legacy: true}],
99 | [require.resolve('@babel/plugin-proposal-class-properties'), {loose: true}],
100 | ]
101 | }
102 | },
103 | {
104 | test: /\.vue\.js?$/,
105 | loader: 'babel-loader',
106 | options: {
107 | presets: [
108 | [require.resolve('@babel/preset-env'), {
109 | modules: false,
110 | loose: true
111 | }],
112 | ],
113 | plugins: [
114 | require.resolve('babel-plugin-transform-vue-jsx')
115 | ]
116 | }
117 | },
118 | {
119 | test: /\.marko$/,
120 | exclude: /node_modules/,
121 | loader: 'marko-loader'
122 | }
123 | ]
124 | },
125 | externals: {
126 | 'hyperapp': 'window.hyperapp',
127 | 'react': 'window.React',
128 | 'react-dom': 'window.ReactDOM',
129 | 'rax': 'window.Rax',
130 | 'vue': 'window.Vue',
131 | 'preact': 'window.preact',
132 | 'inferno': 'window.Inferno'
133 | },
134 | plugins: [
135 | new webpack.DefinePlugin({
136 | 'process.env': {
137 | 'NODE_ENV': '"production"'
138 | }
139 | }),
140 | ]
141 | };
--------------------------------------------------------------------------------
/webpack.server.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var fs = require('fs');
3 | var webpack = require('webpack');
4 |
5 | module.exports = {
6 | target: 'node',
7 | mode: 'production', // 'development',
8 | entry: {
9 | 'server.hyperapp': './assets/src/server.hyperapp.js',
10 | 'server.marko': './assets/src/app/index.marko',
11 | 'server.react': './assets/src/server.react.js',
12 | 'server.rax': './assets/src/server.rax.js',
13 | 'server.preact': './assets/src/server.preact.js',
14 | 'server.vue': './assets/src/server.vue.js',
15 | 'server.inferno': './assets/src/server.inferno.js'
16 | },
17 | output: {
18 | path: path.join(process.cwd(), 'assets/build'),
19 | filename: '[name].bundle.js',
20 | libraryTarget: 'commonjs2'
21 | },
22 | module: {
23 | rules:[
24 | {
25 | test: /\.hyperapp\.js?$/,
26 | exclude: /node_modules/,
27 | loader: 'babel-loader',
28 | options: {
29 | presets: [
30 | [require.resolve('@babel/preset-env'), {
31 | modules: false,
32 | loose: true
33 | }],
34 | ],
35 | plugins: [
36 | [require.resolve('@babel/plugin-transform-react-jsx'), {pragma: 'h'}],
37 | ]
38 | }
39 | },
40 | {
41 | test: /\.react.*\.js/,
42 | exclude: /node_modules/,
43 | loader: 'babel-loader',
44 | options: {
45 | presets: [
46 | [require.resolve('@babel/preset-env'), {
47 | modules: false,
48 | loose: true
49 | }],
50 | [require.resolve('@babel/preset-react')],
51 | ],
52 | plugins: [
53 | [require.resolve('@babel/plugin-transform-react-jsx')],
54 | [require.resolve('@babel/plugin-proposal-class-properties')],
55 | [require.resolve('@babel/plugin-transform-react-constant-elements')],
56 | ]
57 | }
58 | },
59 | {
60 | test: /\.rax.*\.js/,
61 | exclude: /node_modules/,
62 | loader: 'babel-loader',
63 | options: {
64 | presets: [
65 | [require.resolve('@babel/preset-env'), {
66 | modules: false,
67 | loose: true
68 | }],
69 | [require.resolve('@babel/preset-react')],
70 | ],
71 | plugins: [
72 | [require.resolve('babel-plugin-transform-jsx-to-html')],
73 | [require.resolve('@babel/plugin-transform-react-jsx')],
74 | [require.resolve('@babel/plugin-proposal-class-properties')],
75 | [require.resolve('@babel/plugin-transform-react-constant-elements')],
76 | ]
77 | }
78 | },
79 | {
80 | test: /\.preact\.js?$/,
81 | exclude: /node_modules/,
82 | loader: 'babel-loader',
83 | options: {
84 | presets: [
85 | [require.resolve('@babel/preset-env'), {
86 | modules: false,
87 | loose: true
88 | }],
89 | [require.resolve('@babel/preset-react')],
90 | ],
91 | plugins: [
92 | [require.resolve('@babel/plugin-transform-react-jsx'), {pragma: 'h', pragmaFrag: 'Fragment'}],
93 | [require.resolve('@babel/plugin-proposal-class-properties')],
94 | [require.resolve('@babel/plugin-transform-react-constant-elements')],
95 | ]
96 | }
97 | },
98 | {
99 | test: /\.inferno\.js?$/,
100 | loader: 'babel-loader',
101 | options: {
102 | presets: [
103 | [require.resolve('@babel/preset-env'), {
104 | modules: false,
105 | loose: true
106 | }],
107 | ],
108 | plugins: [
109 | [require.resolve("babel-plugin-inferno"), {imports: true}],
110 | [require.resolve('@babel/plugin-transform-runtime')],
111 | [require.resolve('@babel/plugin-proposal-decorators'), {legacy: true}],
112 | [require.resolve('@babel/plugin-proposal-class-properties'), {loose: true}],
113 | ]
114 | }
115 | },
116 | {
117 | test: /\.vue\.js?$/,
118 | loader: 'babel-loader',
119 | options: {
120 | presets: [
121 | [require.resolve('@babel/preset-env'), {
122 | modules: false,
123 | loose: true
124 | }],
125 | ],
126 | plugins: [
127 | require.resolve('babel-plugin-transform-vue-jsx')
128 | ]
129 | }
130 | },
131 | {
132 | test: /\.marko$/,
133 | exclude: /node_modules/,
134 | loader: 'marko-loader',
135 | options: {
136 | 'target': 'server'
137 | }
138 | },
139 | {
140 | test: /\.handlebars$/,
141 | exclude: /node_modules/,
142 | loader: "handlebars-loader"
143 | }
144 | ]
145 | },
146 | plugins: [
147 | new webpack.DefinePlugin({
148 | 'process.env': {
149 | 'NODE_ENV': '"production"', // '"development"',
150 | 'BUNDLE': 'true'
151 | }
152 | }),
153 | ]
154 | };
--------------------------------------------------------------------------------