Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    Animating 2048 SVG nodes in React, Preact, Inferno, Vue, Angular 2, and CycleJS – a side-by-side comparison

    Ever wondered which front-end framework is smoothest to animate 2048 SVG nodes? Here are some GIFs.

    Same dancing pythagorean tree fractal, same mid-2012 retina MacBook Pro. All recorded with LICEcap in a normal, everyday setting running Chrome, Spotify, Emacs, etc. Click on a GIF to see its code.

    --- Angular 2 and CycleJS added on Dec 23rd ---

    People have been asking why this test is important, why should they care, what's the point. It's fun and the demo looks pretty. Fundamentally, it's a showcase of "jank" – how much jankiness is there between what you see on the screen and what you do with the mouse.

    Many of the gifs have live versions linked from their github. Play with them. Maybe even clone the repo and run it locally. It's fun.

    Implementation: mine preact-tree-small Implementation: Dominic Gannaway, creator of Inferno Implementation: Evan You, creator of Vue Implementation: Tero Parviainen, JavaScript consultant Implementation: Wayne Maurer, founder of Lambda IT

    Thanks to Jason, Dominic, and Evan for building the forks. You guys rock!

    And thanks to Tero and Wayne for adding their own versions! So exciting! I hope we get more ? Someone mentioned a raw JavaScript version without frameworks; that would be cool.

    Let's look at the code. For an in-depth explanation of how it works, go here.

    React

    react-tree-small

    Based on Jason's and Evan's tips, I added mouse event throttling to my demo to make it faster. Turns out my original tree wasn't so slow because React is slow. It was slow because I was slamming the rendering engine with so many requests per refresh cycle.

    I tried throttling to requestAnimationFrame, but that didn't work so good. Throttling to React's redraw cycle was easy and works well.

    onMouseMove(event) {
        if (this.running) return;
        this.running = true;
    
        // calculate stuff
    
        this.setState({
            heightFactor: scaleFactor(y),
            lean: scaleLean(x)
        });
        this.running = false;
    }
    

    Check if update is running, do update if not. This works because React's engine is synchronous.

    It might stop working when React Fiber is out. I think. ¯\(ツ)

    Preact

    preact-tree-small

    Jason used the preact-compat layer to make Preact pretend that it's React. This might impact performance.

    What I love about the Preact example is that it uses async rendering to look smoother. You can see the redraw cycle lag behind the mouse movement producing curious effects.

    I like it.

    Here's how he did it: diff on github

    In package.json, he added preact, preact-compat, and preact -compat clones for React libraries. I guess you need the latter so you don't have to change your imports.

    He changed the functional stateless Pythagoras component into a stateful component to enable async rendering.

    // src/Pythagoras.js
    export default class {
      render(props) {
        return Pythagoras(props);
      }
    }
    

    And enabled debounced asynchronous rendering:

    // src/index.js
    import { options } from "preact";
    options.syncComponentUpdates = false;
    
    //option 1:  rIC + setTimeout fallback
    let timer;
    options.debounceRendering = (f) => {
      clearTimeout(timer);
      timer = setTimeout(f, 100);
      requestIdleCallback(f);
    };
    

    My favorite part is that you can use Preact as a drop-in replacement for React and it Just Works and works well. Very promising for future performance optimizations in my current apps.

    Inferno

    inferno-tree-small

    You can use Inferno as a drop-in replacement for React, and at first I did. Dominic says that impacts performance though, so he made a proper fork. You can see the diff on github.

    Dominic changed all react-scripts references to inferno-scripts, and it's a good sign that such a thing exists. He also changed react to inferno-beta36, which means my CTO definitely won't let me use it in production yet.

    From there, the main changes are to the imports – React becomes Inferno – and he changed some class methods to bound fat arrow functions. I don't know if that's a stylistic choice or an Inferno requirement.

    He also had to change a string-based ref into a callback ref. Inferno doesn't do string-based refs for performance reasons, and we need them so we can use D3 to detect mouse position on SVG. It's easier than doing it ourselves.

    // src/App.js
    
    class App extends Component {
        // ...
        svgElemeRef = (domNode) => {
            this.svgElement = domNode;
        }
        // ...
        render() {
            // ..
    
        }
    

    In the core Pythagoras component, he added two Inferno-specific props: noNormalize and hasNonKeyedChildren.

    According to this issue from 8 days ago, noNormalize is a benchmark-focused flag that improves performance, and I can't figure out what hasNonKeyedChildren does. I assume both are performance optimizations for the Virtual DOM diffing algorithm.

    Vue

    vue-tree-small

    This… this took a lot of work. Kudos to Evan and the original forker Phan An!

    Vue doesn't aim to mimic React's API, so this is an almost complete rewrite. I'd show you the diff on Github, but it won't let me do that. You can see the code though.

    You can still recognize the core Pythagoras component. Evan used transform-vue-jsx to enable JSX inside Vue. So we know that's something you can do.

    The main App.vue file though… I'm not used to reading that, so I can't really explain the code. But it looks so familiar.

    Let's try.

    It's split into a <template>, <script> and <style> section. Looks a bit like JSX or HTML, but template props are prefixed with a colon.

    Looks like Vue took the put-it-all-together componentization cue from React but decided to split by language anyway. I don't know if that's good or bad. It looks cleaner, but in my experience, splitting those eventually becomes burdensome.

    The App component still looks roughly like it used to, but it uses data() to define default state, $refs instead of this.refs, a name attribute instead of naming the class itself, a components attribute to define children, and a methods attribute to define class methods.

    Curious ?

    Angular 2

    Implementation: Tero Parviainen, JavaScript consultant

    I've never been a fan of Angular, but this looks pretty good. Although it did feel extremely janky on my machine.

    I don't know why. Maybe all that type checking from TypeScript adds runtime overhead after transpiling?

    The code is a major rewrite, obviously. Tero had to port it all into TypeScript and I find that impressive. I sure wouldn't ?

    I wonder how the language barrier affects reusability of random libraries you find online ?

    The code also looks like it has many more files. App is split into app.module.ts, app.component.ts, app.component.html, and app.component.css. Same with Pythagoras.

    This implies Angular is sticking with the traditional one-language-per-file webdev separation ... although when you look at an Angular html file ...

    <div class="App-header">
      <h2>This is a dancing Pythagoras tree</h2>
    </div>
    <p class="App-intro">
      <svg #svg="" [attr.width]="width" [attr.height]="height" style="border: 1px solid lightgray">
        <g app-pythagoras="" [w]="baseW" [heightfactor]="heightFactor" [lean]="lean" [x]="width / 2 - 40" [y]="height - baseW" [lvl]="0" [maxlvl]="currentMax"></g>
      </svg>
    </p>
    

    That's some funny looking HTML.

    I'm also not sure I understand the difference between a module and a component. It looks like the module declares certain imports, children components, and stuff. But each component still declares its own CSS and template imports.

    Smells like a framework optimized for very large teams. I've never been a part of one of those, so I can't say how good it is for the use-case it was designed for.

    CycleJS

    Implementation: Wayne Maurer, founder of Lambda IT

    Now this… this was smooth on my machine. Maybe it just feels smooth because I looked at it right after the Angular version, but damn.

    Wayne translated everything into TypeScript, but it doesn't look like CycleJS requires that. Despite that, he was able to keep the same simple file structure as the original. I like that.

    It's hard for me to tell what he changed because of TypeScript and what's different because of CycleJS. Looks like he's not using classes to define CycleJS components. The structure looks more like the old school closure approach.

    export function App(sources: Sources): Sinks {
        const factorAndLean$ = sources.DOM.select('#the-svg') //...
    
        const args$ = xs.combine(factorAndLean$, xs.periodic(500) //...
    
        const pythagoras$ = Pythagoras(args$);
    
        const vtree$ = pythagoras$.map(x =>
            div(Styles.App, [ // ...
    
        return {
            DOM: vtree$
        };
    }
    

    This would take some getting used to.

    The part I really dislike is how Wayne specifies the DOM in CycleJS. The main App looks like this:

    div(Styles.App, [
      div(Styles.AppHeader, [
        img(Styles.AppLogo, { attrs: { src: "cyclejs_logo.svg" } }),
        h2("This is a dancing Pythagoras tree"),
      ]),
      p(Styles.AppIntro, [
        svg(
          "#the-svg",
          {
            attrs: {
              height: svgDimensions.height,
              width: svgDimensions.width,
              style: "border: 1px solid lightgray",
            },
          },
          [x]
        ),
      ]),
    ]);
    

    Looks a lot like using the React.createElement approach and I find that hard to read. CycleJS does support JSX however, so I'm not sure why he's not using it.

    That said, the end result is phenomenal.

    Edit on Dec 24th: as @spion pointed out, it turns out the CycleJS example only renders 2^10 rectangles. Half as much as the other examples, due to how .take works. This has a huge effect on the jankiness.

    Edit 2: fixed the code in a local copy, updated the gif, still smooth as hell. I'm impressed.

    Conclusion

    I have no idea which is fastest. They all look smooth to me. Maaaybe Vue is the smoothest. Or maybe Inferno. I like that Preact enables asynchronous rendering. That was cool to see.I'm biased against Angular, and found CycleJS hella impressive.

    React is king in the “Good Enough And I Already Know How To Use It” department.

    I don't foresee migrating my code to Vue, Angular, or CycleJS any time soon. Too much work. Preact or Inferno as drop-ins? Waiting to see what React Fiber does.

    My biggest takeaway from this experiment though: Buy a new computer. When others made gifs it looked smooth as hell!

    If you want to learn more about drawing interactive pictures, building animations, and creating declarative data vizualisations, you should pre-order my new React+D3v4 book. It's coming out soon, and there will be a chapter on leveraging Preact and Inferno for speed optimization.

    Published on December 21st, 2016 in AngularJS, cyclejs, inferno, preact, react, Technical, vue

    Did you enjoy this article?

    Continue reading about Animating 2048 SVG nodes in React, Preact, Inferno, Vue, Angular 2, and CycleJS – a side-by-side comparison

    Semantically similar articles hand-picked by GPT-4

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn more

    Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.

    Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.

    Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.

    Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev

    Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization

    Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections

    Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog

    Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️

    Created by Swizec with ❤️