1 | # Why is Marko Fast?
|
2 |
|
3 | <a href="https://medium.com/@psteeleidem/why-is-marko-fast-a20796cb8ae3">
|
4 | <img src="https://user-images.githubusercontent.com/1958812/28104838-d0182f48-6691-11e7-808d-d1ae2d0fed6d.png" alt="Marko logo" width="100%" />
|
5 | </a><br />
|
6 |
|
7 | You can find the original ["Why is Marko Fast?" article here](https://medium.com/@psteeleidem/why-is-marko-fast-a20796cb8ae3)!
|
8 |
|
9 | At eBay we are using [Marko](http://markojs.com/) to render over a billion
|
10 | requests every day and this has required us to finely tune Marko, our open
|
11 | source UI library. We have heavily optimized Marko for fast rendering, [advanced
|
12 | performance
|
13 | techniques](http://www.ebaytechblog.com/2014/12/08/async-fragments-rediscovering-progressive-html-rendering-with-marko/)
|
14 | and to achieve a minimal page weight (~10kb gzipped). Performance is only one
|
15 | concern because we have also had to scale Marko to support development across
|
16 | hundreds of teams in a way that allows developers to efficiently create
|
17 | maintainable and robust web apps.
|
18 |
|
19 | We have created [our own
|
20 | benchmarks](https://github.com/marko-js/isomorphic-ui-benchmarks) and we have
|
21 | [added Marko to other
|
22 | benchmarks](https://github.com/raxjs/server-side-rendering-comparison/pull/11),
|
23 | but benchmarks cannot always be trusted. While we make every effort to be fair
|
24 | with our benchmarks, what matters most is performance in real world applications
|
25 | as opposed to focusing on micro benchmarks. This is one reason that the V8 team
|
26 | has switched to [a new methodology to measure and understand real-world
|
27 | JavaScript
|
28 | performance](https://v8project.blogspot.com/2016/12/how-v8-measures-real-world-performance.html).
|
29 |
|
30 | Similarly, we’ve taken a look at how our developers are _actually_ writing their
|
31 | Marko components and have found patterns that could be further optimized.
|
32 | Instead of focusing on benchmarks in this article, I want to focus on the
|
33 | details of optimizations that we have applied to Marko.
|
34 |
|
35 | ### Multiple Compilation Outputs
|
36 |
|
37 | Marko is an isomorphic UI library that runs on both the server and in the
|
38 | browser. As [Michael Rawlings](https://medium.com/@mlrawlings) mentioned in
|
39 | “[Server-side Rendering
|
40 | Shootout](https://hackernoon.com/server-side-rendering-shootout-with-marko-preact-rax-react-and-vue-25e1ae17800f)”,
|
41 | when rendering on the server, Marko renders directly to a string representation
|
42 | of the document (HTML) that can be sent as the HTTP response.
|
43 |
|
44 | When rendering in the browser, an HTML string would have to be parsed in order
|
45 | to update the DOM. For this reason, Marko compiles a view to a program that
|
46 | renders directly to a virtual document (VDOM) tree that can be used to
|
47 | efficiently update the real DOM when targeting the browser.
|
48 |
|
49 | Given the following template:
|
50 |
|
51 | ```marko
|
52 | <div>Hello ${input.name}!</div>
|
53 | ```
|
54 |
|
55 | #### Compiled for the server
|
56 |
|
57 | The compiled output is optimized for streaming HTML output on the server:
|
58 |
|
59 | ```js
|
60 | var marko_template = require("marko/html").t(__filename),
|
61 | marko_helpers = require("marko/runtime/html/helpers"),
|
62 | marko_escapeXml = marko_helpers.x;
|
63 |
|
64 | function render(input, out) {
|
65 | out.w("<div>Hello " + marko_escapeXml(input.name) + "!</div>");
|
66 | }
|
67 | ```
|
68 |
|
69 | #### Compiled for the browser
|
70 |
|
71 | ```js
|
72 | var marko_template = require("marko/vdom").t(__filename);
|
73 |
|
74 | function render(input, out) {
|
75 | out
|
76 | .e("DIV", null, 3)
|
77 | .t("Hello ")
|
78 | .t(input.name)
|
79 | .t("!");
|
80 | }
|
81 | ```
|
82 |
|
83 | The compiled output is optimized for virtual DOM rendering in the browser:
|
84 |
|
85 | ### Modular Runtime
|
86 |
|
87 | The Marko runtime is not distributed as a single JavaScript file. Instead, the
|
88 | Marko compiler generates a JavaScript module that will only import the parts of
|
89 | the runtime that are actually needed. This allows us to add new features to
|
90 | Marko without bloating existing applications. For example, given the following
|
91 | template:
|
92 |
|
93 | ```marko
|
94 | $ var color = 'red';
|
95 | <div style={backgroundColor: color}></div>
|
96 | ```
|
97 |
|
98 | In the above example, extra runtime code is needed to render the `style` attribute
|
99 | based on the JavaScript object that is provided. The compiled code that imports
|
100 | the `styleAttr` helper is shown below:
|
101 |
|
102 | ```js
|
103 | var marko_styleAttr = require("marko/runtime/vdom/helper-styleAttr");
|
104 |
|
105 | function render(input, out) {
|
106 | var color = "red";
|
107 | out.e(
|
108 | "DIV",
|
109 | {
|
110 | style: marko_styleAttr({
|
111 | backgroundColor: color
|
112 | })
|
113 | },
|
114 | 0,
|
115 | 4
|
116 | );
|
117 | }
|
118 | ```
|
119 |
|
120 | ### High performance server-side rendering
|
121 |
|
122 | Compared to solutions based on JSX that exclusively do virtual DOM rendering,
|
123 | Marko has a huge advantage for server-side rendering. When rendering to a
|
124 | virtual DOM tree on the server it’s a two-step process to render HTML:
|
125 |
|
126 | - First pass to produce an entire virtual DOM tree in memory
|
127 | - Second pass to serialize the virtual DOM tree to an HTML string that can then be
|
128 | sent over the wire (this requires traversing the entire tree structure)
|
129 |
|
130 | In contrast, Marko renders directly to an HTML stream in a single pass. There is
|
131 | no intermediate tree data structure.
|
132 |
|
133 | ### Compile-time optimization of static sub-trees
|
134 |
|
135 | Given the following template:
|
136 |
|
137 | ```marko
|
138 | <div>This is a <strong>static</strong> node</div>
|
139 | ```
|
140 |
|
141 | Marko will recognize that the template fragment produces the same output every
|
142 | time and it will thus create the virtual DOM node once as shown in the following
|
143 | compiled output:
|
144 |
|
145 | ```js
|
146 | var marko_node0 = marko_createElement("DIV", null, 3, ...)
|
147 | .t("This is a ")
|
148 | .e("STRONG", null, 1)
|
149 | .t("static")
|
150 | .t(" node");
|
151 |
|
152 | function render(input, out) {
|
153 | out.n(marko_node0);
|
154 | }
|
155 | ```
|
156 |
|
157 | Rendering a static sub-tree has virtually zero cost. In addition, Marko will
|
158 | skip diffing/patching static sub-trees.
|
159 |
|
160 | Similarly, on the server, Marko will merge static parts of the template into a
|
161 | single string:
|
162 |
|
163 | ```js
|
164 | function render(input, out) {
|
165 | out.w("<div>This is a <strong>static</strong> node</div>");
|
166 | }
|
167 | ```
|
168 |
|
169 | ### Compile-time optimization of static attributes
|
170 |
|
171 | Marko will also optimize static attributes on dynamic elements.
|
172 |
|
173 | Given the following template:
|
174 |
|
175 | ```marko
|
176 | <div.hello>Hello ${input.name}!</div>
|
177 | ```
|
178 |
|
179 | Marko will produce the following compiled output:
|
180 |
|
181 | ```js
|
182 | var marko_attrs0 = {
|
183 | class: "hello"
|
184 | };
|
185 |
|
186 | function render(input, out) {
|
187 | out
|
188 | .e("DIV", marko_attrs0, 3)
|
189 | .t("Hello ")
|
190 | .t(input.name)
|
191 | .t("!");
|
192 | }
|
193 | ```
|
194 |
|
195 | Notice that the attributes object is only created once and it is used for every
|
196 | render. In addition, no diffing/patching will happen for static attributes.
|
197 |
|
198 | ### Smart compiler
|
199 |
|
200 | With Marko we favor doing as much at compile-time as possible. This has made our
|
201 | compiler more complex, but it gives us significant gains at runtime. We have
|
202 | ~90% code coverage and over 2,000 tests to ensure that the compiler is working
|
203 | correctly. In addition, in many cases the Marko compiler provides hints to the
|
204 | runtime for a given template so that the runtime can optimize for specific
|
205 | patterns. For example, Marko recognizes if an HTML element only has `class`/`id`/`style` defined
|
206 | and the runtime optimizes for these virtual DOM nodes when doing
|
207 | diffing/patching (the Marko compiler generates code that flags simple virtual
|
208 | DOM nodes for targeted diffing/patching logic).
|
209 |
|
210 | ### Event delegation
|
211 |
|
212 | If you are building a UI component you will likely need to write code to handle
|
213 | various DOM events (`click`, `submit`, etc.). It is common for developers to write code that
|
214 | adds DOM event listeners using `dom.addEventListener(...)` or using a library such as jQuery. You can still
|
215 | do that when building UI components using Marko, but there is overhead in
|
216 | attaching listeners when lots of components are being initialized. Instead,
|
217 | Marko recommends using declarative event binding as shown below:
|
218 |
|
219 | ```marko
|
220 | <button type="button" on-click("handleClick")>
|
221 | Click Me
|
222 | </button>
|
223 | ```
|
224 |
|
225 | When using declarative event binding, no DOM event listeners are actually
|
226 | attached for events that bubble. Instead, Marko attaches a single listener on
|
227 | the root DOM element of the page for each DOM event that bubbles (done at
|
228 | startup). When Marko receives an event at the root it handles delegating the
|
229 | event to the appropriate components that are interested in that event. This is
|
230 | done by looking at the `event.target` property to see where the event originated and then
|
231 | walking up the tree to find components that need to be notified. As a result,
|
232 | there is slightly more work that is done when a DOM event is captured at the
|
233 | root, but this approach uses much less memory and reduces the amount of work
|
234 | that is done during initialization. The extra overhead of delegating events to
|
235 | components will not be noticeable so it is a very beneficial optimization.
|
236 |
|
237 | _Cover image credit: [Superhero by Gan Khoon Lay from the Noun Project](https://thenounproject.com/search/?q=superhero&i=690775)_
|