Search code examples
javascriptreactjssveltevirtual-domsvelte-3

SvelteJS vs ReactJS rendering difference (repaint / reflow)


Here's my naive understanding of how the DOM and browser works

Whenever something in the DOM ( the real dom ) changes the browser repaints or reflows that the DOM. So in simpler terms every time the DOM changes browser needs to recalculate the CSS, do a layout and repaint the web page. This is what takes time in real dom.

So React comes with this virtual DOM and what it actually does is it batches the changes and call applies them on real-dom in one go. Thus, minimizing the re-flow and re-paint.

Then what about Svelte. If it is manipulating the DOM directly how does it controls the repaint/reflow of the browser.


Solution

  • In addition to the (correct) answer above: Svelte "compiles" the code that you provide to it, so the final code can be executed without a library runtime (in contrast to React). And it creates rather readable code, so it is absolutely possible to understand the inner workings.

    Note: This will be a bit longer answer - and still leave out many minute details about what is going on under the hood of Svelte. But I hope it helps to demystify some of the aspects what is going on under the hood. Also, this is how Svelte does things as of v3.16.x. Since this is internal, it might change. However, I still find it always rewarding to understand what is really going on.

    So, here we go.

    First and foremost: The Svelte tutorial has a useful feature to let you see the generated code (right next to the "Result" pane). It might look a bit intimidating at first, but you quickly get the hang of it.

    Below code will be based on this example (but further simplified): Svelte tutorial - reactivity/assignments

    Our example component definition (i.e. App.svelte) looks like this:

    <script>
        let count = 0;
    
        function handleClick() {
            count += 1;
        }
    </script>
    
    <button on:click={handleClick}>{count}</button>
    

    Based on this component definition, the Svelte compiler creates a function which will create a "fragment" which receives and interacts with a "context".

    function create_fragment(ctx) {
        let button;
        let t;
        let dispose;
    
        return {
            c() {
                button = element("button");
                t = text(/*count*/ ctx[0]);
                dispose = listen(button, "click", /*handleClick*/ ctx[1]);
            },
            m(target, anchor) {
                insert(target, button, anchor);
                append(button, t);
            },
            p(ctx, [dirty]) {
                if (dirty & /*count*/ 1) set_data(t, /*count*/ ctx[0]);
            },
            i: noop,
            o: noop,
            d(detaching) {
                if (detaching) detach(button);
                dispose();
            }
        };
    }
    

    The fragment is responsible to interact with the DOM and will be passed around with the component instance. In a nutshell, the code inside

    • "c" will be run on create (creating the DOM elements in memory and also setting up the event handlers)
    • "m" will run on mount (attaching the elements to the DOM)
    • "p" will run on update, i.e. when something (including props) changes
    • "i" / "o" are related to intro/outro (i.e. transitions)
    • "d" will be run on destroy

    Note: The functions like element or set_data are actually very approachable. For example, the function element is just a wrapper around document.createElement:

    function element(name) {
        return document.createElement(name);
    }
    

    The context (ctx) will hold all instance variables as well as functions. It is nothing more than a simple array. Since Svelte "knows" what each index means at compile time, it can make hard references to the indices at other places.

    This code essentially defines the instance context:

    function instance($$self, $$props, $$invalidate) {
        let count = 0;
    
        function handleClick() {
            $$invalidate(0, count += 1);
        }
    
        return [count, handleClick];
    }
    

    Both the instance method as well as the create_fragment will be called from another function call init. It's a bit more involved, so instead of copy and pasting it here, you can have a look at this link to the source.

    The $$invalidate will make sure that the count variable is set as dirty and schedule an update. When the next update runs, it will look at all "dirty" components and update them. How this is happening is really more of an implementation detail. If interested, set a breakpoint in the flush function.

    In fact, if you really want to go a bit deeper, I recommend to clone the template app, then create a simple component, have it compiled and then inspect "bundle.js". You can also debug the actual code if you either delete the source maps or deactivate them.

    So, for example set the rollup.config.js like so:

        output: {
            sourcemap: false,
            format: 'iife',
            name: 'app',
            file: 'public/build/bundle.js'
        },
        plugins: [
            svelte({
                dev: false,
    

    Note: As shown above, I recommend to also set dev mode to false since this will create more concise code.

    One neat feature: Once our app is running, you can also access the app variable (it is assigned to the global window object since it is bundled as an immediately-invoked function expression).

    So, you can open your console and simply say

    console.dir(app)
    

    which will produce something like this

    App
        $$:  
            fragment: {c: ƒ, m: ƒ, p: ƒ, i: ƒ, o: ƒ, …}
            ctx: (2) [0, ƒ]
            props: {count: 0}
            update: ƒ noop()
            not_equal: ƒ safe_not_equal(a, b)
            bound: {}
            on_mount: []
            on_destroy: []
            before_update: []
            after_update: []
            context: Map(0) {}
            callbacks: {}
            dirty: [-1]
            __proto__: Object
        $set: $$props => {…}
    

    One cool feature is that you can the $set method yourself to update the instance. For example like so:

    app.$set({count: 10})
    

    There are also Svelte DevTools which try to make the internals of Svelte more approachable. Somehow, they seemed to affect the render performance of my apps when I personally tried them out, so I don't use them myself. But certainly something to watch.

    Well, there you have it. I know this is still rather technical, but I hope it helped to get a better understanding on what compiled Svelte code is doing.