https://javarome.medium.com/design-noframework-bbc00a02d9b3 Home Notifications Lists Stories --------------------------------------------------------------------- Write Jerome Beau Jerome Beau Follow Feb 27 * 25 min read Design: #noFramework Is it as hard as you think? [1] Is the framework layer really a good thing? Trendy ones used to be Angular, then React, now Vue.js... others like Ember, Backbone or Knockout have nearly disappeared. Standard ones like Web Components are seldom used, "yet another framework" seem to ship every year, like Svelte, Aurelia, and each one is now featuring its server side counterpart (NestJS, NextJS or Nuxt for the first mentioned ones, Sapper for Svelte, etc.). Not speaking about non-Javascript web frameworks (Django, Spring, Laravel, Rails, etc.). There's even frameworks over frameworks (Quasar, SolidJS) and, at the end of that spectrum, NCDPs. This multiplicity is confusing for both developers who want to know which technology is worth learning, and deciders who have to make strategic choices. Pretending to help, comparison articles are published or updated as often. However most of them are usually biased as "I worked with this one" versus "I tried others a bit". Less biased authors will conclude that with a "it depends" (on performance, tooling, support, community, etc.), which is just another way of being inconclusive. Even benchmarks, by comparing the same application on every framework, can hardly provide a realistic comparison, as limited by the scope of dummy app (such as a todo list). In the end, frameworks look like religions (or politics): each of them pretend to have the solution, but each of them is different. Each of them claim to provide the best vision of the world an app, but there are heated debates about which one holds the truth. Each of them require you to follow specific rules and, while there may be similarities, it is always difficult to convert from one to another. Let's look at an "atheist" approach of frameworks: use none. Where am I talking from? Aside from a 25+ years of professional experience in software development, this article is based on experience in building a real-world vanilla JS web app (front and back). Why not using frameworks? Actually the idea is now new. As back as 2017, Adrian Holovaty, co-creator of the Django web framework, spoke about his own frameworks fatigue and why he left Django to build his own vanilla JS project. Holovaty's talk at dotJS 2017 One may wonder why would someone want to dive into such a (supposedly) hard task as building a web app without a framework. Why not building an app on top of previous work, using robust frameworks with hundreds of years of combined engineering hours? Is it a NIH syndrom that will eventually lead to building a custom framework? No. Developers are not more inclined to masochism than the general population. Actually, they are probably more lazy than anyone: they want to write less code (so they get less bugs), they want to automate processes (to avoid human mistakes)... [1] ...but they also want to be agile, that is, to be able to handle any problem easily and so, quickly. While "quick" sounds akin to one promise frameworks make (to save you time by doing plumbing for you, while increasing reliability), this doesn't come for free: they want you to sign a contract to agree with a tax and put your code into a silo*. The Framework Tax Framework services come with a cost. They require you to: * comply with their API so that they can provide your their services. This is just the way a framework works: your code will have to adhere to some rules, including more or less boilerplate code. So it's the framework way, or the highway. Your daily challenges will be less about "how to do this" than "how to make the framework (not) do this". Dodge those constraints at your own risks: if you bypass a framework by directly calling low-level APIs, don't expect it to understand what you're trying to do, don't expect it to stay consistent. So it's a false promise frameworks make that you'll be "focusing on your business": in reality you have to care on the framework too, and a lot. * upgrades are effectively forced if you: 1) want a new feature (even if you didn't wanted all those of the next release, you need to upgrade the whole thing) or 2) want a bugfix, or 3) want to avoid loosing support (as new versions are shipped, the one on which you have based your app will get deprecated). Upgrades can also be lacking and let you frustrated (and possibly with a project at risk) with an identified bug but no planned date for a fix. Third-party framework-specific libraries (such as widgets) or plugins are no exception to that rule and will be less and less compatible with your app if you keep using old versions. Maintaining backward compatibility has became such a hassle for frameworks maintainers that they now find more profitable to work on tools that automate upgrades of your code as much as possible (Angular's ng-update, React native Upgrade helper, Facebook's jscodeshift, etc.). * train to learn how they work (what they can/cannot do, what are their concepts, APIs, ecosystem, tools), including changes that may occur in new versions. Should you pick the most popular framework of the day, this might be easier, but it's unlikely that you'll ever know about every aspects of a given framework. Also, hype comes and goes: should you decide to use another framework for a new app (or even worse, to migrate from one to another), the cost of investing in such proprietary knowledge will be lost. This explains a lot of inertia in enterprise projects, even if each project is different than the previous one. "Compatibility means deliberately repeating other people's mistakes," said the late David Wheeler. * compromise with the drawbacks implied by delegating control: you may not be able to do whatever you want (or to prevent the framework from doing things you do not want) or you may not achieve the performance you want (because of additional layering, too-generic code, bigger code size or backward compatibility requirements). * loose skills. A number of developers either don't know much about the lower-level APIs (because they always used the framework layer instead) or live in the past (i.e. are stick on an outdated knowledge of it, not being aware of the latest improvements and new capabilities). The law of the instrument then leads too often to build overkill solutions to simple problems, and loose (if even once acquired) knowledge to build simpler ones. Being guided by blueprints and recipes, they loose (or not gain) a culture of good software design (principles, patterns) and barely build a significant engineering experience. Just like users of CSS frameworks (Bootstrap, Tailwind, etc.) lack CSS skills, users of web frameworks are doomed to lack experience in both modern web APIs and software design in general. [1] Once you put your money inside a framework, it's hard to get it out. The Framework Silo Aside the "tax" that you have to pay to get their benefits, frameworks can also induce an additional major issue when they are not standard. As they enforce rules -- but each one of them is different -- this implies binding your app with a proprietary ecosystem. That means locking your app code with a proprietary API (and its upgrade process). That's a risky bet for your project, as it implies: * no portability: migrating your code toward another framework (or a new version of it with breaking changes, or even vanilla code) will be very costly, including the cost of possible re-training ; * no interoperability of your code with other frameworks runtime or other framework's components libraries that you'd like to use: as their rules are different, most frameworks do not interoperate easily one with each other. Of course you can get reassured by selecting the most popular framework... at the time of your project starts. That may be acceptable for an app that is quite short lived, but not for a long term investment. [1] Frameworks come and go. Their fate is a decline in interest, replaced by 1 to 3 new ones per year, since 2018. Standard frameworks, however, don't imply this silo effect. On the web platform (i.e. the browser framework), using standard web APIs makes your investment less risky are they are expected to work on most browsers. If not all (or not all the ones you target), support can still be provided through polyfills. For instance, Web components are today both portable (they can be used in nearly all browsers) and interoperable (can be used by any code, including proprietary frameworks) as they are encapsulated as any HTML element. Even better for performance, their runtime (Custom Elements, Shadow DOM, HTML Templates) is executed as part of the browser, so it's both already there (i.e. not downloaded) and native. [1] Rare image of developers trying to escape a framework silo. So are frameworks bad by nature? No, if only because coding an app almost always results in creating your own framework: any app implement its own business rules, that apply to its business objects. So frameworks are a good thing if either they: * are app-specific: any app ends up designing its own "business" framework. * are standard or end up with a standard. For instance the web platform is a standard web framework, and Web Components frameworks (lit, stencil, skatejs, etc.) end up building components that comply with the standard. * add some unique value that you're missing in all other alternatives (including other frameworks). In such a case you have almost no choice, as the unique added value justifies the implied cost of locking with it. For instance, an OS-specific framework makes sense since it enforce OS standards and there is no other other way to provide an app or extension for it. * are used to build non-critical apps (short lived, with lower quality expectations) where tax and silo effect are acceptable. For instance it makes sense to use Bootstrap to build some prototype, MVP or internal tool. The goal So, in a nutshell, avoiding a framework to build an app aims to: * maximize flexibility by avoiding "one size fits all" constraints from frameworks. Also, not having blueprints avoids the law of the instrument to increase the creativity for ambitious applications. Most web apps using Bootstrap can be recognized as such, because they're having a hard time getting out of the predefined components and styles. In the end, they'll have a hard time thinking another way. * minimize dependency to any of the currently hyped frameworks. Not being locked with a framework avoids issues with portability and interoperability. * maximize performance by allowing the most fine-grained operations only when required (no framework-dependent refresh cycle for instance) and reducing dependencies to a selection of precise, required-only, set of lightweight libraries. And, of course, the goal is neither to "reinvent the wheel". Let's see how we can do that. The alternative So, what is it to build apps without framework tax and silo? First, we must clarify the anti-goal: "building an app without a framework" is NOT to be confused with "replacing the framework". This is not the challenge at stake: a framework is a general purpose technical solution to host virtually any app, so it is less about your app than all apps. On the opposite, going vanilla is an opportunity to focus on your app's needs only. [1] Developing an app without a framework doesn't mean to re-implement the framework. This is an important scope narrowing to make to assess the (non-) difficulty of building your app without a framework: it is not as hard as building a framework, because you do NOT aim to build: * a proprietary component model (a container implementing a specific components lifecycle) * a proprietary plugins/extension system : * a fancy template syntax (JSX, Angular HTML, etc.) * optimizations that make sense for general-purpose (change detection, virtual DOM) * framework-specific tools (debugging extensions, UI builders, version migration tools) So building a vanilla app is not an enormous task of "reinventing the wheel" as often caricatured, because the major part of this "wheel" is actually about the APIs/contracts, their implementations, the general-purpose engine and associated optimizations, the debugging capabilities, etc.. Leaving the general-purpose goal and focusing on your app's goals means that you can rid of most of it. Ironically, this is the real "focus on your app" approach. Now, how to design and implement a vanilla app? When most of apps are built using a framework, it may indeed be hard to devise a way to achieve similar results without that familiar instrument. You'll have to: * change your state of mind: don't look for the framework-specific services mentioned above. As a vanilla app, you will probably don't need it. Don't think change detection, just update the DOM, etc. * use technical alternatives for the common tasks you performed with frameworks (updating the DOM -- including reactively -- , loading lazily, etc.) This latter topic has been addressed by authors like Jeremy Likness (who has devised some bits of JS to provide common framework facilities to a vanillaJS app) or Chris Ferdinandi (a.k.a. "the vanilla JS guy") but, by definition, any vanilla app may choose to use one of those techniques or not, depending on its needs. For instance, the authors of MeetSpace didn't need much more than the standard APIs. Let's look at a number of common recipes, though. Standards As we have seen above, standards APIs are among the "good frameworks" as they: * allow portability: they are expected to be available everywhere. When not yet available, they can be polyfilled. * allow interoperability: they can interact with other standards and be used by proprietary code. * are long lived: as devised by multiple industry actors rather than only one, they are well designed and here to stay once released. So investing in them is less risky. * are immediately available in the browser most of the time, which avoids downloading them. In some case you may have to download polyfills instead but, contrary to proprietary frameworks (which are doomed to be less and less trendy), their fate is to be more and more available (thus reducing download probability). One could also consider that the choice of the programing language should focus on standards. JavaScript have evolved over the years, and now contains features usually provided by other languages like the class keyword, or limited type checking support through JSDoc comments such as @type. A number of languages transpile to JavaScript: TypeScript, CoffeeScript, Elm, Kotlin, Scala.js, Haxe, Dart, Rust, Flow, etc. Each of them adds different values to your source code (type checking, additional abstractions, syntax sugar); should a vanilla app use them? To answer that question, let's look if they imply the same drawbacks as frameworks: * comply with their syntax: by definition, most languages enforce this (CoffeeScript, Elm, Kotlin, etc.) with the notable exception of those which are supersets of JavaScript (TypeScript, Flow) which allows you to write some the parts of your choice using pure, lower-level JavaScript. * upgrade can be required if you use very old versions of any language (including JavaScript itself) but at a very lower pace than frameworks. * train, by definition again, is required to use their syntax. However superset languages allow you learn progressively, since you can stick on traditional JS in some parts of your code. * loosing skills about the target language (JavaScript) is indeed a risk for non-superset languages, as the transpilation/compilation is general-purpose, can be non-optimal, and you may be unaware of it. Maybe you could have performed the same operation with more simple and efficient JS code. * compromise with the drawbacks is indeed required, as you cannot change the transpilation to JS (or just customize it a bit, using tsconfig.json for instance) nor the compilation to WebAssembly. Some languages may also omit some JS languages concepts. * portability is achieved, as transpilation can usually target ES5 (but sometimes you will have to compromise with that target even if you'd wanted to target ES6). WebAssembly is more recent but supported by all modern browsers. * interoperability with other JS code is provided or not. Typescript can be configured to allow JS for instance. As you can see, you should we wary about the source language to use in a vanilla app, as all of them imply more or less constraints. Superset languages (TypeScript, Flow) allow to minimize those constraints by avoiding an "all or nothing choice" and can be used only where it adds value. In any case, keep in mind that adding a language layer above JavaScript implies a layer of complexity in your tooling chain that may fail for some reason (see below). Also, development-time benefits are lost after compilation/transpilation (type checking or restricted visibility might not be enforced at runtime typically). Libraries As for the "rewrite a framework" false assumption, it is often considered that vanilla JS apps are NOT supposed to use libraries. This is utterly false. Once again, "reinventing the wheel", i.e. rewriting everything from scratch cannot be a sensible goal. The vanilla goal of removing constraints implied by frameworks and not libraries, must not be confused with a "write everything by yourself" dogma. So there is nothing wrong in using libraries as code that you cannot write by yourself (because you don't have time to do it, or because this requires too much expertise). All you have to care about: * modularity: avoid using a big lib if you use only a small percentage of it; * avoid redundancy: only use a lib if there is no standard (or polyfill of it), and prefer libs that implement a standard; * avoid locking: don't use the lib API directly, wrap it in your own app API. Oh, and don't be fooled by frameworks documentation or articles that would claim that they are not a framework (because they would be "unopinionated", or not defining a "complete application", etc.): as soon as they imply a contract, they are. Patterns As Holovaty says, the option to just applying patterns to structure your software (instead of using frameworks) is not considered enough. Those patterns are well-known and are not specific to vanilla development. They are themselves self-documenting since they are quickly recognized by experienced developers (providing you name them correctly). To name only a few: * splitting Model, View and Controller (MVC); * factories to create objects depending on configuration; * observers to ease reactive programming; * iterators to virtualize collections (lazy load their elements); * proxies for lazy loading, security checks, etc. * commands to encapsulate operations that might be triggered from various contexts. This list is not exhaustive, nor required: you are free to use what fits your needs, but when one pattern provides a typical solution to a typical problem of your app, you should definitely apply it. More generally, anything that fulfills SOLID principles and a good cohesion is good for your app flexibility and maintainability. Updating When interviewing developers about what would be their primary concerns when trying to build a vanilla application, most of them reply that it would be complicated to implement model change detection and subsequent updates in the relevant "views" of the app. This is a typical law of the instrument effect, which makes you think in a framework way, whereas not being a framework actually implies much more simple needs: 1. The "views" are just DOM elements. You can abstract them of course (and you should) but in the end they are just that. 2. Updating them is just a matter of viewElement.replaceChild (newContent). That's it. No unnecessary update of a larger DOM scope, no unwanted redraw or scrolling. There are several ways to update the DOM, from inserting text to manipulating real DOM objects. Just pick the one that fits your need. 3. "Detecting" when updating is required is usually not necessary in a vanilla app, since most often you just know what is to be updated following an event as you can just do it imperatively. You grab your DOM target and update it, period. In some cases of course you might want to do a more generic update by reversing the dependency and notifying observers (see below) that will update themselves. Templates Another feature that developers fear to miss is the ability to write HTML snippets with dynamics parts, even listeners, etc. First of all the DOM API (document.createElement("button"), etc.) is not that hard, and actually more powerful than any template language since this allows you full access to the API. It can be tedious to build long HTML fragments but, hey, if they are that long, it's probably that you need to split it in more fine grained components. It is true, however, that viewing those elements as a template improves readability. So how to have them? There are actually multiple ways: * HTML Templates are now available in browsers (since 2017 at worse). They provide the ability to build a reusable, off-screen, HTML