You (probably) don't need a JavaScript framework

I’m not going to post yet another rant about “Why the JavaScript community is so bad” or anything like that, because I don’t feel that way. I’d much rather show you that it’s actually pretty simple and surprisingly fun to do things from the ground-up by yourself and introduce you to how simple and powerful the Web API and native DOM really is.

For the sake of simplicity, let’s assume your everyday framework is React but if you use something else, replace React with X framework because this will still apply to you too.

React, Virtual DOM, Webpack, TypeScript, JSX…

HOLD ON! Let’s pause for a second here and think.

Let’s start from the beginning and ask ourselves why do these things exist in the first place?

Virtual DOM is efficient and has good performance, but native DOM interaction is insanely fast and your users will not notice any difference. This is where you have to choose what sacrifice you want to make. Add 45KB of extra size to your app to save a couple of milliseconds for DOM manipulation that is completely invisible to the user.

Stress-testing tools like DBMON is good to have for scientific purposes, but all too often I've seen these tests being used in arguments between developers to decide which framework is the best. The reality is that it is a stress-testing tool and nobody does that many DOM manipulations in reality, and if you do you seriously have to reconsider to use WebGL instead (clarification; by that point, my guess is that you're trying to build a game using the DOM tree).

With optimized vanilla I get 46 repaints/sec while optimized React gives 36 repaints/sec, so I don't know what's up with that?

I'll redact that, it's irrelevant and doesn't contribute to the post.

React was created to solve the issues Facebook were facing with their massive amount of data, activity and size. They then went ahead and open-sourced it and released it to the public, where many developers seem to have come to the conclusion that “Facebooks’ solution to their problem is the solution to my problem”.

This - in the majority of cases - is not true.

UPDATE (30/4 17:30 CET):

Many have raised concerns about using React in this context. I've adressed some of these concerns at the bottom of this post, especially application state-related arguments.

Additionally, please read through the whole post if you're going to make a statement - there's been way too many reactions that has already been accounted for (e.g mentioning features that isn't available in every browser is adressed in the polyfilling section).

Don't underestimate the browser

The browser is a very powerful platform, considering most implementations have cross-platform support with a lot of additional functionality.

The native Web API and DOM API is more than enough to power your application and gives you really good performance, meanwhile having great loading times across all platforms.

The underlying JavaScript engines like V8, SpiderMonkey and Chakra is almost scary fast considering JavaScript is a runtime-interpreted, dynamically typed language. I've seen V8 rival a few compiled, statically typed languages in terms of performance, and that is not something you should take lightly. I am very thankful for the hard work these engine developer teams has put into their respective JavaScript engine. The web would not be as capable as it is today without them.

Solution

What you need is vanilla JavaScript and the Web API. I want you to take a look at the Web API (detailed). Scimm through it for a couple of minutes and come back.

Done? Awesome. I was pretty impressed when I first looked at it in detail. A lot has changed and it's much simpler to work with. Take MutationObserver for example - it allows you to watch parts of the DOM for mutations. This feature alone would allow you to setup simple databinding and react to potential changes.

Here's an example of observing a contenteditable h1 tag:

let elementToWatch = document.querySelector('h1')

elementToWatch.contentEditable = true



let observer = new MutationObserver(mutations => {

  mutations.forEach(mutation => {

    console.log(mutation.target.textContent)

  })

})



observer.observe(elementToWatch, {

  subtree: true,

  characterData: true
})

This is a fairly useless example, and you would rather want to observe DOM structure mutations with it, but you get the idea.

Another example is the Fetch API. It's like XHR, but much more straight-forward and powerful.

UPDATE (30/4 17:30 CET):

When I first started web development 6 years back, the Web API & DOM API were indeed a pain to work with, but times have changed and the ECMAScript updates alone has turned the tables quite a bit. Syntatical changes has been made to the language that plays much nicer with some parts of the Web API that were previously non-trivial to work with. New features are constantly being rolled out that accounts for many of the reasons you are working with a framework in the first place, most of which can be polyfilled where not available.

The new Proxy language feature allows you to implement reactivity from A to Z, among other things.

Decorators allows you to add additional functionality to parts of your code.

Class inheritance allows you to start from a base and extend additional features to your app as it evolves.

This again, is a question of what your application needs. It might not even need reactivity, it might not even need to decorate functionality - but nontheless it is there, in the browser and available to you whenever you need it without any additional overhead to your application (unless polyfilled).

This post has blown up to becoming "how you replace your framework with vanilla JavaScript", and that was not the intention. The idea is that you should take a moment and reflect on your reason for using a framework. You have to ask yourself if your application really is all that complex that you absolutely require solutions to the vast amount of problems a framework tries to solve.

You most definitely don't have many of those problems in the first place if you think about it:

  • You don't have problems reflecting state onto the UI if you think about it.
  • You don't have problems with data storage or caching if you think about it.
  • You don't have problems with writing reusable code/components if you think about it.
  • You don't have problems with large, complex UIs if you think about it.
  • You don't have problems with performance if you think about it.

Just because most companies are using frameworks doesn't mean they have to either. Companies aren't some higher form of being, they consist of developers and normal people that has made a choice of using a framework because they think it is the right to do. If it is a good choice or not depends on their application(s).

Write functions and classes

Things that you often use in your application should of course be placed in their own functions or classes, depending on if you need instances of something or not.

This could for example be setting up observers, walking the DOM or talking with a server-side resource.

A great example of using classes is if you want a lightweight implementation of components. An instance of a specific class can represent a component and be its view-model.

You could also build a tiny router and utilize HTML5 History API.

Some may argue that this is like reinventing the wheel, but it really isn't because you are the one in control. You are the author of your code, and you get to work with code you feel comfortable with. You can build it however you want and support whatever browser versions you want.

Take a look at router.js, this is what their README says:

router.js is a lightweight JavaScript library that builds on route-recognizer and rsvp to provide an API for handling routes.

The library is 24.4KB minified, and 2,175 lines of code after transpilation.

How many lines of code do YOU think that YOU can write a router for your application in? 100? 200? 1000? How about 20?

I'm not saying that you shouldn't use libraries, because you should. But I want you to be critical when choosing what libraries to use. Check through the source, maybe submit a pull request and try to help reduce the size a bit if you find something that's unnecessary.

Know when to use JS and when to use CSS

CSS is powerful too. It can handle almost every animation/transition that you can imagine without the need of any JavaScript involved.

Only animate the transform and opacity attributes, and never things like height or width. A bit more about this here.

Use :hover, :active and :focus for simple triggers for something like a dropdown menu. You can also trigger child elements via these methods.

What about browser support?

Browser support is usually something you have decide upon a project-basis, but if there's a feature that isn't available for a browser you want to support, just polyfill it. Granted, there are some features like Service Worker that can't be polyfilled, but if a feature can't be polyfilled, a framework can't have support for it either.

An argument that often comes up when talking about polyfilling is that it increases the size of your application. That is true, however you can dynamically polyfill based on what the client browser already has support for. And the follow-up argument is that dynamic polyfilling adds additional round-trips to the server, and that is however NOT true in modern web, explained below.

HTTP/2

The HTTP-protocol has been completely rewritten. The protocol is no longer textual, but binary. Binary protocols are much more efficient and less error-prone. HTTP/2 connections are also multiplexed which - to put it simply - allows you to transfer multiple responses simultaneously within a single connection.

Additionally, HTTP/2 implements server-push. And this is the feature I was getting at earlier when I was talking about polyfilling.

Server-push allows your server to send additional data that the client may not have requested. For instance, a client requests index.html and your server responds with index.html, style.css, script.js, polyfill-1.js and polyfill-2.js.

This doesn't solve the application size problem, but it reduces the number of round-trips to the server. However, you can reduce the size by adding server-side configuration to match the requesting browser version and dynamically decide which polyfills to push on a per-client basis.

This is where Webpack and other module loaders/bundlers are falling behind, because bundling is bad practice with HTTP/2. The only reason bundling is being used is to reduce the amount of requests made to the server, but since HTTP/2 is multiplexed, implements server-push and can independently cache each asset this is no longer the case. If you would bundle and still use HTTP/2, you wouldn't get all the caching capabilities HTTP/2 offers. Because imagine if you only update a single line in a JS-file, the user would have to download the entire bundle again (even the code that hasn't been updated) and cache it. Without bundling, only the changed file would have to be re-downloaded - thus reducing bandwidth drastically for websites with large amounts of traffic. It also makes your site much more mobile-friendly as bandwidth and parsing is a huge deal on mobile.

Additionally, you could serve a bundled application for HTTP/1.x visitors and a non-bundled application for SPDY & HTTP/2 visitors.

HTTP/2 is widely supported by web browsers already. At the time of writing, 70.15% of visitors has support for the updated protocol. The problem is that hosting providers and website owners haven't enabled it in their web server configurations yet.

Application structure

When you start a project, it’s always a good idea to plan ahead and estimate roughly what you need. If it’s a truly single page site (1 page), you definitely don’t need a framework. A single index.html, styling and optional script is enough. If you want to use Stylus and ES2015 for example, you can npm init, install Babel and Stylus and use their command-line versions in a npm run script. You could also add watch for a more snappier development environment.

package.json:

{
  ...
  "scripts": {
    "build": "babel src -d dist & stylus src/styles -o dist/styles & cp src/index.html dist/index.html",
    "dev": "watch 'npm run build' ./src"
  }
  ...
}

npm run build and you have a production-ready build.

There's a small, but great Gist that you can look at for more snippets.

Package manager & Module loader

When your application grows large, it could be an option to use a package manager. I recommend JSPM as it is based on SystemJS which in turn is based on the standard ES6 module loader spec.

Know though that this introduces the same problem as Webpack with HTTP/2 support, so you might as well just use NPM and and build a simple script that copies your dependencies during build.

Using copy-paste of libraries is not a bad way of handling your dependencies. That's the way it was before package managers for the browser existed and the current package manager implementations - except for bower - inject some kind of script into your production build due to having support for module loading. Module loading introduces many problems due to the cheer amount of different module formats and does not currently play very well with HTTP/2. Until that's fixed, you may as well just stay with NPM as I mentioned earlier or copy-paste what you need. This is of course also a question of application size and the amount of dependencies you have.

Conclusion

I really hope that I've inspired you to try out native web development at least one more time. If you for some reason don't feel comfortable with the native Web APIs or DOM APIs and want to stick to your framework, I don't blame you. Do what you feel comfortable with and try to make the best out of the situation!

Lastly, I want to apologize that this had to be a Slack post.

Cheers, Tom.

UPDATE:

I've read some comments and feedback about mentioning React. I apologize that I didn't go more in-depth into it, but I'll try to cover as much as possible now.

Note though, that my message for you was not that your framework is bad - it was for you to ask yourself if you really need it. Play a bit with the thought, open your mind and try to picture yourself re-building your previous project using vanilla JavaScript, native Web API and DOM API.

How much more time do you think it would take?

How much bandwidth would you save and would it be much more complicated?

I understand that React is only the view-layer, but in reality many of you are using things like Redux with it - along with other plugins. The result of that is a pretty heavy application.

React is not about performance, it's about handling UI state, and that is hard.

Yes, but that's not my message for you. UI state isn't hard for the size your applications are. A plain JavaScript object holding the top-level state, and using Object.observe (or Proxy since Object.observe has been made obsolete) should be more than enough for you to implement automatic UI state synchronization functionality, across components. It doesn't have to be more complex than that. I can definitely understand why large companies like Facebook and YouTube need such functionality, but it doesn't make sense for your blog editor or e-store with 7 categories.

Building your own implementations makes it hard for the next person that comes in to do maintenance of your work.

That is indeed a problem, but with proper commenting and documentation it shouldn't be. It's not like you're writing a new framework - you're writing wrappers for functionality you often re-use. In the end it's plain JavaScript and Web APIs. Just because you don't use a framework anymore doesn't mean that you have to build something overly complex. And in the case of your web app becoming part of a fortune 500 company, you'd probably write your own framework either way as Facebooks solutions would probably not solve your problems.

JavaScript size doesn't matter that much when there are images > 500KB.

Unfortunately it does. The JavaScript engine has to parse, interpret and execute the code. Most frameworks runs heavy tasks in the startup-phase of the document, adding even more time to first paint. This is really bad on mobile, even if they have a good connection.

It will turn out bad, spaghetti code - impossible to continue development of.

For starters, you've got to learn the how to write clean, reusable code. As mentioned earlier, document your code!

You haven't built any complex applications.

I've built a few complex applications with offline-support, notifications and data that is synchronized in real-time. Don't really know what more to answer, feels like a completely irrelevant question.

I've worked with Angular, React, Aurelia, Angular 2.0, jQuery and Polymer. None of them are very pleasing to work with, Aurelia is probably my favorite for its unobtrusiveness.

My message for you

My point was not that your framework sucks or anything like that, I wanted to inspire you to try out the native DOM and Web API again. There's a lot of new features and stuff that you probably haven't seen yet.