Fast rendering with DOM template engines

    Boris Kaplunovsky ( BSKaplou )


    Boris Kaplunovsky

    I worked on the report for quite some time and tried to make it as controversial as possible. And right away I’ll start with a contradiction - I fundamentally disagree with the fact that web components can be used. The question has already been raised about 300 Kbytes, I am deeply convinced that 300 Kbytes for a Javascripta page is unacceptably a lot.

    Today I will talk about a rather deep journey to the front-end. This journey began when I found that the front-end aviasales.ru is slow, and something needs to be done. This journey began a year and a half or two ago, and the things that I will talk about are a condensed narrative of what I learned.

    The most critical, in my opinion, in the performance of front-end applications is rendering. We all know that working with the DOM is such a thing that you should try to avoid. The more calls you make to the DOM API, the slower your application runs.



    What exactly will we talk about? About the rules of the game. What kind of things are in the rendering, in the operation of the web application, you need to pay attention to what parameters are key for the template library for rendering, what types of template engines are.

    Next, I will walk a little along the bones of the giants, these are AngularJS and ReactJS, I will try to tell you why I do not like them and why they slow down. I’ll tell you that I found good things in other template engines and about the work that we created based on all the above knowledge.


    Probably part of the audience is interested in what the diver at the bottom of the screen means? Our development team is located in Thailand, and I personally dive. An analogy was born in my head: if you are under water, the less movements you make, the more oxygen you save, the more you can swim. With the DOM, we see roughly the same thing - the fewer calls to the DOM you make, the more likely it is that the user will not encounter brakes.



    Let's start about the rules of the game. User experience depends on the speed of page initialization. We are all deeply passionate about page caching, but I have to immediately contradictory declare that caching does not work. It does not work, because the first contact with a site in a person is the most critical. If the site slows down during the first load, then the second time the user may not return to you. The initial loading of the page is crucial.

    The second important thing is the responsiveness of the interface. If a person clicked on a button or checkbox, and the interface did not respond immediately, the user can close the site and go to another site, where the interface is responsive.

    The next thing is resource consumption. On web pages, two main indicators are important: processor consumption (if you do a lot of unnecessary actions, you heat the processor, and it does not have enough time to calculate the animation on the interface or simply draw something), in addition, if you create a lot unnecessary objects, this puts a strain on the garbage collector. If you create a load on the garbage collector, then it will be called periodically, and the responsiveness of your application will drop.

    And the last, but from this no less important point. Library size If you have a single page application, then 200-300, sometimes even 400 KB of javascript you can afford. However, the component web, in the direction that we are moving merrily, implies that the pages are built from different web components. Moreover, these web components are often produced in different companies and come with their own package.

    Imagine a page on which a dozen widgets are inserted: a widget for exchange rates, weather, airline tickets, a dash in a mortar of which ... And each of these components weighs 300 KB, and this is only JS. Thus, we can easily get a page that weighs 5-10 MB. Everything would be fine, and the Internet is becoming faster and faster, but mobile devices have appeared, slower networks have appeared, and if you use the Internet not in the city of Moscow, but somewhere in Yekaterinburg, then the 15 MB website will turn out to be an absolutely unacceptable luxury for you. That is why the size of the library, in my opinion, is critical.

    A little lower I compare several libraries, and I do not compare polymers, I do not compare for the reason that 200 Kbytes for a web components library is too much.



    So, let's move on to the topic of conversation - to the templating.

    All of us who are engaged in web development are already accustomed to string template engines. String template engines are template engines that return a string to us as a result of their work. The line that we insert later with innerHTML in html. This is a wonderful, ancient, familiar mechanism. However, it has several disadvantages. The main drawback is that every time you make a template and paste in innerHTML, you have to throw out the whole DOM that was there before and insert a new DOM.

    As far as I remember, working with the DOM is very, very slow. If you threw 20 tags with 30 attributes and inserted the same 20 tags with 10 attributes, then this will take a considerable time. 20 milliseconds is easy. In addition, string template engines do not allow you to leave anchors for quick updates of single attributes, single text nodes, etc.

    Having discovered these suboptimities, we began to look for how to get rid of these shortcomings, what can be done about it? And the first thing Goggle suggested was “Use the DOM API”. This thing is not very simple. But she has pluses.



    This is a screenshot from jsperf. A benchmark that sees the performance of string template engines that insert html pieces from innerHTML and JS DOM. Here we see the performance on Android at the top, and we see that the JSDOM API allows us to speed up the rendering several times. Here, about three times. At the same time, there is no such hellish performance gain on desktop browsers.

    About half a year ago, Google began promising all mobile developers “moboheddon”. This means that all sites that are not adapted for mobile devices, responsive, adaptive, will be pessimistic in search results. This means that if you are not ready for mobile devices, simply the traffic from Google on your sites will significantly decrease.

    In fact, this slide clearly shows that using the DOM API, you can significantly speed up rendering on mobile devices. And this applies not only to Androids. As you know, all modern Androids and iOS devices use the same WebKit engine, with approximately the same optimization set, which means that you will get the same performance increase on all iOS devices if you render pages through the DOM API.



    However, the DOM API is rather cumbersome. Here I have listed five basic calls with which you can create DOM portions. I cited them approximately in the form in which they will appear in the code of your program if you create DOM sections directly through the API.

    Creating one element, which used to fit in 15-17, can be 30-50 characters, through the DOM API you can easily pour out 5-10 lines of code. Programmers' working hours are valuable, which means that we cannot replace html with manual DOM programming.



    Here we need template engines. As you recall, string template engines are slow, and you want to have DOM template engines, template engines working through the DOM API, but allowing you to use all the goodies that we are used to working with ordinary template engines.

    So, what do the DOMs give us, apart from the ability not to use the native JSDOM API? They allow you to save DOM objects in variables for quick updates later. Using DOM template engines, you can use the same DOM portion several times.

    What I mean? Suppose we visit a web store page. Users enter the same category of goods, and data on one list of goods is substituted into the prepared templates. When a person goes into another category of goods, other data is substituted into the same templates. In fact, we do not recreate the DOM, we use the same parts of the DOM to display data. This allows you to save a lot on processor resources, memory, and sometimes programmers' time.

    After realizing this thought that the tool that I need is DOM template engines, we went to see what already exists in the industry, what can be used to quickly and efficiently work with DOM, and quickly render it?

    Next I will tell you where, in my opinion, the giants stumbled.



    The first giant I want to talk about is AngularJS.

    AngularJS, it seems to me, stumbled right at the start. If you used it, you probably noticed that all the templates are sent to the client as either DOM sections (which is not a very good style) or as strings. After the library has loaded, Angular is forced to compile your lines or DOM into real templates. This happens on the client.

    Imagine an interesting situation. The user visits the page, loads all JS, which for Angular applications can be quite a lot - 100-200-300 Kbytes easily. After that, each line-parsing template begins to compile. This leads to only one thing - the initial loading of Angular applications can last half a second, a second, due to this compilation (during which users do anything other than work with the site). I met sites on which the compilation process of the template took even two seconds. Moreover, this problem grows like a snowball: the more templates in your application, the more complex your single page application, the more time we spend on the initial compilation of templates.



    The next problem is in Angular. We all remember that Angular was sold to us by the gentlemen of Google, as the first coolest two-sided binding framework. Moreover, this two-way binding is implemented through the so-called. The $ watchers that you hang on data structures for later displaying them in the DOM. There is an interesting picture on the slide, but you don’t look at it. The only interesting thing in it is this wonderful cycle, during which all $ watches are used for all the data that you have in the system. Moreover, of course, in the documentation and in all the tutorials, no one will tell you that you need to follow $ watchers. It leads literally to the following. At some point, your wonderful application starts to slow down once every 100 ms. Animations begin to slow down, memory begins to flow. It turns out that it’s simply impossible to allow a lot of $ watchers. As soon as you have made a lot of $ watchers, your application starts to slow down spontaneously. Here you start to subtly tricky, go for anything, reduce the number of $ watchers, refuse the application of the two-way binding, for which you took Angular, only to get rid of the brakes.



    In addition, it seems to me that Angular’s ​​architectural flaw is that Angular doesn’t have the only properly described way to work with the DOM. The directives are practically independent, each of them works with the DOM as it sees fit. But it turns out that going through the Angular directives, we can mark some directives fast, some as slow, and some directives as very slow.

    If you used ng-repeat, then you probably saw that if you cram 100 elements into it, and there will also be $ watchers, then all this will take a very long time to render. The problem is so wide that when working with Angular (our previous version of the output was built specifically on Angular), we had to write our own ng-repeat. This was done by our employee Anton Pleshivtsev and talked about it at many conferences. In addition, 50 KB of the minimized library size, in my opinion, is still a bit much. Those. what are you paying for? If you look at the Angular code, then these 50 Kbytes have their own class system, the version of Underscore, in my opinion, is of a very poor quality. And all this you get absolutely free of charge within the framework of 50 Kbytes of code.



    Following. A much better framework, in my opinion, is ReactJS. Judging by how the Internet is bubbling, every first programmer, not even always a front-end vendor, used Angular and was delighted with it. I do not think that virtualDOM can speed up work with the DOM.

    See what virtualDOM offers us. VirtualDOM is the source from which ReactJS creates the real DOM, i.e. in addition to the real DOM, from the creation of which you can’t get anywhere (virtualDOM just allows you to create it), ReactJS also holds virtualDOM in memory, this is called redundancy.

    VirtualDOM is slightly smaller than the real DOM, maybe 5 times. However, in fact, you are forced to keep two copies of virtualDOM in memory. Those. you hold the real DOM, you hold the reflection of the real DOM in virtualDOM, in addition, every time you are going to make a div in virtualDOM, you make another copy of the DOM. You had one DOM, now you have three of them - well done! Moreover, for each data change, you create another copy of virtualDOM, this is the third copy, however, you create it from scratch.

    This puts a serious strain on the garbage collector and on the processor. In addition, in my opinion, the library is still oily - 35 KB. And again, the guys decided to draw their class system, draw their lowdash, the original for some reason did not suit, and all this was stuffed into 35 KB. In addition, there is packed virtualDOM with a mythical algorithm that supposedly gives tremendous performance.

    The next problem with virtualDOM and React in particular is that ReactJS knows nothing about the semantics of your data. Let's see this very simple example.



    Here we see two nested
    , and another tag is embedded in them . To change value via virtualDOM, the virtualDOM algorithm inside React is forced to compare three tags and one text value. If we know the semantics of the data, for us it is enough to compare only the text value, simply because the template says that inside one
    always different
    inside the next
    tag ... Why do we need to compare them every time? This is really an overhead.

    In addition, if you programmed in React, then you are familiar with such a thing as pure-render-mixin. Its essence is to get rid of working with virtualDom. There is a very interesting situation close to comic. First, the gentlemen from Google sold us a couple of years React as a thing that with the help of virtualDOM hellishly speeds up work with the DOM, and then it turns out that in order to quickly work with the DOM, you need to exclude virtualDOM. Well done, well done.

    And now something else. I wanted to search - maybe there are libraries on the planet, there are people who have done something better. I didn’t try to find one library, a silver bullet, but I wanted to spy on libraries with things that could be used either to speed up React or to create my own library. And here is what I found.



    I will consider two interesting libraries. The first of these is RiotJS.

    In my opinion, RiotJS is the correct AngularJS, simply because the library size is 5 Kbytes. The guys took exactly the same ideas that were in AngularJS, but did not rewrite lowdash, just said: “Why? He is already written. " The guys did not rewrite, invent their class system, did not do anything. Got a 5 kb library. Performance is greater than AngularJS, the ideas are exactly the same. Moreover, the templates used in RiotJS use data semantics, which gives a good performance gain. But the problem remained - the compilation of the templates is still happening on the client. It's not very fast, but much better already.



    The next library that caught my attention was PaperclipJS.

    PaperclipJS uses a number of very interesting optimizations. In particular, cloneNode is used to create templates, and then I will show that it gives a big increase in performance, but this solution allows PaperclipJS to be more transparent, more understandable for the developer.

    But this library also had two drawbacks: it is quite large - 40 Kbytes, this is more than React; and despite good ideas, development is rather sluggish. This library has been a couple of years old, however, it still has not left the beta stage.

    Having talked with these libraries and other libraries, having read the html5 guru, I was able to come up with the following list of techniques that can speed up work with the DOM.



    The first thing is VirtualDOM. I searched for its advantages for a long time, and found only one - it allows you to reduce the number of calls to the DOM, thereby increasing productivity. However, the overhead for creating a copy of the DOM, in my opinion, is still significant. A sophisticated comparison algorithm, over which there is still a veil of secrecy, which is used in React, it is not as fast as we are promised about it. To understand how it works, you will spend two days. And all this magic, which was described in blogs, is not there, in my opinion. In addition, virtualDOM sits on the problem that the algorithm knows nothing about the data structure. While we do not know anything about the data structure, all of our vrappers, all of our layout elements, negatively affect performance, because the virtualDOM algorithm must be involved in comparing them.



    Techniques that have been known for a very long time are the use of cloneNode, which I already mentioned in the framework of PaperclipJS and DocumentFragment. These two techniques are used to increase productivity. None of the techniques, as far as I know, are used in either AngularJS or ReactJS. However, a screenshot of the benchmark with jsperf clearly shows that this allows you to speed up work with the DOM by at least three times. Pretty good practice, I highly recommend using it.



    The next technique, which lies absolutely on the surface, moreover, is implicitly found even in the React tutorial, is to create DOM sections in advance. What I mean? Suppose a person visits a page of an online store of electronic dummies. Introduces the name of the teapot, the name of the company of the teapot that wants to purchase. At this point, a search request is sent to the server. If your server programmers are fast and lightning fast, then you can get a response in 20 ms, the user does practically nothing with these 20 ms. And at this moment we can create a DOM structure for the data that will return from our server. Pretty simple practice. I do not know why it was not widely used. I use it, it turns out very cool.

    Total, what is obtained? We send a request to the server, while we wait for a response from the server, we prepare the DOM structures for the data that should come to us from the server. When an answer from the server comes to us, in fact, we still need to parse it. More often than not, it’s not just about accepting Json, but somehow adapting it. If by this moment the DOM is ready, then we can spend the 2-3-4 ms that we have for JS to adapt and insert data into the DOM and add data to the page.

    I strongly advise you to use this, and explicitly in frameworks this thing is not supported, but you can create an element with your hands when sending a request to the server.

    So, equipped with all this knowledge, and finding a little free time at night and on weekends, I decided to write a small prototype, with which we began to work further.



    This is the templating temples. It is very simple, very small, there are literally less than 2,000 lines of code.

    What properties does he possess? Templates are compiled at the time of assembly in JavaScript code, i.e. no work is done on the client except loading JavaScript code. The ability to create DOM chunks in advance is supported right in the library. The library makes it easy and easy to reuse templates. The size of the library, in my opinion, is more than modest in a minimized and gzip form - it's only 700 bytes. Moreover, the thing that I like the most about it is updating the DOM as quickly as possible.

    Next, we will try to parse into pieces how this is all done and working.



    The structure of the template is extremely simple and primitive. This is a mustache inside which variables are substituted.



    Everything is pretty obvious, no magic. In addition, it supports two designs. This is forall key iteration for loops and if branches. Expressions are not supported. Some time ago, before the advent of React, the industry was dominated by the view that you should in no way interfere with View and the model. I still believe that this is the right approach, so if you want to use complex expressions, it is better to put it in a separate component. If you remember, there are such Presenter or ViewModel patterns, if you need to prepare display data in templates, it’s better to do it there and not drag expressions into templates.

    Next I will show how to work with it. I believe that you don’t need to create a framework for everyone, that the future of the web, and especially the component web, in very small and independent libraries that do not require a change of religion and cutting out everything from the program to integrate into them.

    What does work with the template look like.



    We take the named template from the template, update the data in it with the update call. And paste it into the DOM. In fact, this is very similar to working with the regular DOM API. After we used the template, we can remove it from the DOM by calling remove. Everything is very simple.



    How is the early creation of the DOM done? Suppose we sent a request to the server and we know that a set of dummies should come from the server. At this moment, we say the template pool: "Create a cache for 10 dummies." It is created, and the next time we will make the same call to get, there will be no real work with the DOM, we will get a prepared and shredded template. Those. Get the template to insert into the DOM instantly.

    When is it most convenient to use? Look, we send a request to the server and we have 20 ms, in fact, of course, not 20, most likely it is 200-300 ms, during this time we can cache millions of DOM nodes, i.e. enough time.

    The second option is to cache templates when we expect DOMContentLoaded.

    There is such a problem with DOMContentLoaded that a lot of handlers subscribe to this event, and as a result, at the time of the arrival of this event, a damn cloud of scripts wakes up that everyone starts to process on a callback, and after this event the application sleeps for about 100 ms. It deeply considers something there. To reduce this expectation, you can do the caching of DOM templates in advance, before this event arrives to us.



    Quick changes to the DOM. Here I give a simple and clear call, it is very similar to how update is done in React. Everyone remembers the call in React setState, the difference is only in the depth of the stack. If you saw how deep, how many function calls React makes before you make the target action (and the target action with us is more likely to be this, simply substitute this value in the DOM), then you know that for React it can be stack depth 50 -60, maybe more.

    Every challenge, especially in dynamic languages ​​like JavaScript, is not at all free. It is not as slow as a DOM call, but still not free. Temple allows this substitution with stack depth = 2, i.e. essentially, update is called, a function is called from it that replaces this value. This is essentially a value function. And with stack depth = 2, we get the target action. In this case, the update call is recommended to be used when we want to change several values ​​at once. If we want to change one, then this can be done even faster - by direct calls to property and then this substitution will be done with stack depth = 1, it is physically impossible faster.



    Reuse of templates. After the user has made a search query for teapots, seen enough of teapots, and made a new search query, we can return the templates to the pool for further use, so that we can reuse them later. This is also supported by the framework, such a pool.release function.



    Next, I will try to sell this tool to you using benchmarks. The first benchmark. Below I always give a link to the benchmark, and remember that on jsperf “more” means “better”. In this case, red is Temple, and blue is React. I compare C with React, because React is probably the fastest solution, it is 5 times faster than Angular usually. So what do we see here? The initial initialization in Temple is done 30 percent faster in Chrom, and 10-15 percent in Firefox. How is this obtained? Inside, in the end, the same create element, create text, node appendchild are used. However, in Temple there is almost never a stack depth greater than two. In fact, we save time exclusively on calls within the library. The 35 Kbytes of JavaScript you download to use React,



    The next benchmark that we invented and drove away is soft Update. Soft Update is when we substitute in the template the same data that has already been substituted there. This is where virtualDOM should wake up and say: "Guys, the data is already there, nothing needs to be done." I will say right away that for the purity of the experiments on virtualDOM I did not use pure-render-mixin. And it turned out that the optimizations in the browser itself allow you to do this four times faster. VirtualDOM brakes four times.



    Let's move on, hard update. Hard Update is a scenario in which not one piece of data is updated in the template, but all the data that is. Again, we do not use pure-render-mixin, but it would be useless here. And we get even more interesting data. With hard update, Temple's win is tens of times, simply because there is no virtualDOM.

    VirtualDOM turned out to be a very resource-intensive operation in practice. And if you deeply programmed React-applications, then quite quickly faced with the fact that working with virtualDOM needs to be reduced. I have achieved complete perfectionism in this idea and believe that virtualDOM, as an idea, is bad, and it needs to be thrown out. This will make React lighter by 20 KB, and 10 times faster.



    The template we did is very small. Aviasales is not Facebook, we do not have millions of engineering hours, we only have ... As you know, the development of such libraries is not a very product feature, and it doesn’t work out during working hours. This can be done at night by a small group of enthusiasts. Therefore, the library is very small. Temple does not offer event handling. React has a DOM delegate; AngularJS has its own event handling. But I don’t think that it is necessary to integrate work with events into the template engine. You can use standard libraries to work with events. We use domdelegate from Ftlabs. Ftlabs is the Financial Times IT division. The guys made a very good, very simple and productive library, its size, if I am not mistaken, is less than 5 Kbytes. We use it in tandem with Temple, and are pleased with the results.



    In favor of my previous benchmarks and words, I want to say that at the moment we are using Temple in two projects: this is the mobile version of aviasales.ru and the new search results for aviasales.ru. In the mobile version, all templates are converted to 10 Kbytes of code, we are talking about minimized and compressed code, and the entire application takes 58 Kbytes. It seems to me that this is a good size for a rather complex single page application.

    The next application that we developed was a new search engine, where the templates already occupy 15 Kbytes, and the entire application takes 70 Kbytes. There were also several integrations with widgets, but they are less interesting. However, 70 Kbytes for a single page application, I think this is a good indicator. Especially when compared with libraries that weigh 200.



    Actually, it is open in open source now. You can watch it, play with it on the url on the slide. It is still pretty damp.

    As he said, we do not have a huge amount of resources to evangelize this work of ours in order to make documentation. If interested, you can go, there is documentation, there are examples, there are plugins for gulp and grunt and there you can see good performance.

    Contacts


    " BSKaplou
    " bk@aviasales.ru

    This report is a transcript of one of the best speeches at the conference of frontend developers FrontendConf . We have already opened preparations for 2017, and by subscribing to the conference mailing list you will receive 8 of the best reports of last year.

    The most difficult section of the upcoming conference HighLoad ++ is " Performance frontend ." The frontend has become large, it is already a full-fledged software with its architecture, models and data (and not just an interface, as it was before). It is in this section that we study it in this section.

    Here are some of the upcoming reports:



    Also popular now: