BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Presentations Enhance: SSR for Web Components

Enhance: SSR for Web Components

Bookmarks
40:41

Summary

Brian LeRoux discusses Enhance, a way to build web apps with the pure web standards.

Bio

Brian LeRoux is co-founder of Begin.com, the creator and maintainer of OpenJS Architect, an open-source framework for generating and deploying AWS standard SAM/CloudFormation and coined the term FWA. Also a maintainer of the recently shipped Enhance.dev.

About the conference

QCon Plus is a virtual conference for senior software engineers and architects that covers the trends, best practices, and solutions leveraged by the world's most innovative software organizations.

Transcript

LeRoux: My name is Brian LeRoux. I'm very excited to be here at QCon SF, to share with you Enhance, which is a new HTML framework. Before I get into, what is an HTML framework, I think it's important to level set and talk a little bit about how we got here and why you should care. Have you ever had a dependency break? Of course, you have. The reason I want to bring this up is, I don't know if we always really see this happening and for what it is. When a dependency breaks in your system, you obviously have to go and fix it. Usually, that's unplanned work. You don't plan to have an upstream dependency come down and break on you. When it does, now you're on the hook to fix it to keep existing functionality alive, which is time you could have spent not working on that dependency, instead of adding new value. As we like to say in Canada, does a bear poop in the woods? They do. Unplanned work is a real problem in our industry today. Unstable dependencies are just simply a time sink. They're very common. Maybe they're not totally necessary. That's a question that we've been asking a lot lately. Bezos famously got asked the question, what's going to change in the next 10 years? He's a bit of a futurist. He almost never gets the question, what is not going to change in the next 10 years? He believes that that second question is more important, because you can build a business strategy around things that are stable with time. This is true of our dependencies. If we're choosing dependencies that are unstable, then it's really difficult to execute a business strategy and respond to customer needs and add value if we're just chasing breaking changes, and spinning our tires, keeping the existing functionality alive.

Software is complicated. It isn't made less complicated by more dependencies. Arguably, it's made worse by more dependencies. Yet, in our industry, we are constantly building on top of each other, creating more dependencies. This isn't really all that new. In the past, we've had some very complicated dependencies in the web, in particular, with web browsers. As different browsers matured, and as time went on, they all had slightly different implementations of similar functionalities. These differences led to a lot of thrash and unplanned work. Web browsers used to be really broken. The different browsers and the different browser ecosystems were very incompatible, and they would take very small capabilities and work on them differently. This led to things like jQuery coming around to fix that problem. The good news is, web browsers don't do this anymore. Web browsers generally do not break their contract. We have solid web standards that ensure consistent behavior. Even better, in the last few years, browsers started upgrading themselves. Back in the early days, if you installed a new Windows operating system and got Internet Explorer, that was the version you got. It wouldn't necessarily upgrade until you chose to upgrade it. The same was true for all the other browsers. Nowadays, browsers are what we call evergreen, which means that they upgrade themselves, which is really nice, because that means they're forwards compatible. Because the web standards are also backwards compatible, so websites from years ago still work today. That's profound. It's worth bringing this back up, web browsers are backwards compatible. Breaking changes are generally an optional thing that you can choose to opt into. Just because you compile the web standards doesn't necessarily mean that you get to enjoy them. This is the crux of Enhance in our whole talk.

Breaking Change, and Additive Change

The question that's worth asking is, can user learned source code be stable in time? Can we make breaking changes optional or opt in? I believe the answer is yes. The two kinds of change that exists are breaking change, or additive change. Breaking changes are when we remove APIs or interfaces or behaviors and add new ones. Additive change is exactly that, it's just adding a new API. We don't take away the old API and replace it with a new one, we keep the old one and we add a new one. There's tons of examples of this in time. Breaking changes are really common, especially in the JavaScript ecosystem. Angular 1 to 2 famously broke everybody. React 18 introduces some new semantics, which can break you. TypeScript 4.8 completely changed how TypeScript transpiles down to Node modules, that's very breaking. That's unfortunate, because none of that is necessary. Additive change, like what the browsers do, is when we iterate and we add new interfaces. HTTP/1 to HTTP/2, you didn't have to do anything, it just happened. XMLHTTPRequest was the original way to do networking in the browser. Nowadays, we use Fetch. You got to choose when you decide to adopt Fetch. You didn't have to switch to a new interface, and then have your old interface break. You got the option to upgrade yourself. Similarly, we used to do things called promises, or continuation passing, or callback KeyCode. Nowadays, a lot of time, we just use Async/Await. That was additive change. That was a new interface. The same thing for modules with JavaScript. There was a new type for scripts. We just got that change. Font, similarly, woff, woff2, additive change. Interestingly, Amazon does this as well with their APIs. The Amazon Web Service is very famous for having great stability. It comes at a cost. That cost is you'll end up with some interfaces that look like this, which isn't really all that great, but better than a breaking change.

Templating Systems

The frontend is very complicated. It's grown very complicated, because it takes on whole new responsibilities. It's a vast ecosystem too. It's not just React. It's React. It's Svelte. It's Astro. It's Angular. It's Knockout. It's Backbone. The list just goes on and on. The worst part about all this, these littles projects all over the place are incompatible with each other. They often will introduce subtle breaking changes that will break their own ecosystems. There's a few reasons for this. The first crack at bringing a lot of this logic into the browser was using templating systems. One of the more popular templating early systems was a thing called EJS, which was largely inspired by ERB, Ruby land. EJS lets you have some syntax with the opening paren and percent sign, and you can extensibly write a little bit of JavaScript inside of your markup. You can use that to template HTML content. We did all this because earlier versions of JavaScript didn't have a string template literal. We have one of those today, and that's worth remembering. We took a page from earlier runtimes, like Java, or Ruby, or Python and created these templating languages. It's nice, but it's like this little embedded language inside of your language. Let's face it, this syntax is hard to read, and parse, and can be error prone. Templating languages generally evolve now into transpiling. Transpiling is a very specific term where we take text and we compile it into text. There's not compiling, where we take text and we compile it to object code, we take text and we compile it to text. Usually, we take a non-standard dialect like JSX, and we compile it to standards-based language like JavaScript. JSX is really nice. You get a declarative JavaScript like interface, which compiles down to real JavaScript. This does require a transpilation step, and this code will not run in a web browser. This is a little bit of a misnomer to call this app.js, it's app.jsx. It's not going to run in a web browser as this is not standard code. This can't change. The semantics can change and that creates debugging problems.

At least some of the non-standard dialects are really embracing it. Svelte has to get credit. They at least have a good file extension and openly will admit that they are creating a new language and they're compiling to web standards. I think this is a much better approach. At least we're being upfront with our tradeoffs here. There's still the same problems with templating languages. You have to learn this new language. You have to understand what all these symbols mean and where they go. This isn't HTML, this is like HTML. This isn't JavaScript, this is like JavaScript. That's going to create problems. All credit to Svelte for being upfront about that. They will claim this results in more efficient code. I don't think that's necessarily always true, however. The big problem with transpilers, and to a large extent, templating systems, as well, is that this creates obfuscated runtime code. This code is pretty bad. You can't read this code. Debugging this code is going to be very difficult following it around and trying to trace what's happening.

It even gets worse from here. It's not just unreadable or hard to diagnose, it's huge. Look at this? This is the same file, 1200 lines of code. This is a trivial bundle that I pulled from a popular framework's Hello, World. This is not efficient. This is terrible as a developer experience to debug. If you've got an error in this code, you're going to be dropping console log statements to figure out what's going on. Some people will object and say, no, source maps fix this. They often don't work, and especially don't work in environments where it really counts like your backend in Node, which just got source map support and it's not all that great. The other issue with transpiling code in the modern JS ecosystem is that we're often stuck with these static artifacts. Because we've compiled it, and we've taken all this runtime logic and put it into Author-time, in order for us to do anything dynamic, we're going to need spinners and loading screens. Because that code is static, it doesn't have dynamic data. It's not being run at runtime. It's being precompiled and prerendered as best as it can. For anything dynamic, you're left with spinnerammer, which is very janky.

Progressive Enhancement

There are just too many moving parts here. This is really complicated. The question that we've got to ask is, what can we get rid of? Can we simplify this situation? Can we get to the root cause of this complexity? How do we avoid it? To me, this is a very big case for progressive enhancement, which is a proven practice for building resilient websites. Sometimes people ask, what is progressive enhancement? The basic idea with progressive enhancement is that you start with working HTML. Then you add a script tag, and you enhance the behavior with JavaScript. Sometimes people call this HTML first lately. I like that, because I think that's very honest with how the browser lifecycle works. HTML is first, that's how a browser works. It loads the script tag, and then JavaScript is second. This doesn't mean JavaScript doesn't load or don't use JavaScript. This just means that it embraces the lifecycle and the workflow of it. There are other cases for progressive enhancement. One of the big reasons that people will often bring up are ethical, for better accessibility. Some devices, screen readers, in particular, don't interpret JavaScript as well, so having working HTML is generally a good idea. Then you can obviously add JavaScript on top of that, if the device supports it. Happy days, everybody gets the best possible experience.

There's also a fully selfish reason here, aside the, avoiding being ableist and saying you don't need to be accessible. That selfish reason is that you will have a more resilient website. You'll have a website that always works, it's more robust. People will argue, no one disables JavaScript. I think that's probably true. No one does really disable JavaScript. JavaScript does fail a lot. When it does fail, your app will still work. Saying my plan is to not fail is cute, but that's not planned. Progressive enhancement is to start with working HTML and add a script tag when you need it. Very often, you won't. Very often, for many sites, working links and forms are going to be good enough. By all means, bring JavaScript to the party to make it better, but don't assume that you need that first because that's not how browsers work.

HTML first is not just a thing that some fringe weirdos are talking about. This is a movement, and this movement is getting bigger by the day. I think a lot of the old school web devs have seen this as the right way to build for a very long time. A lot of sites aren't Node centric, so they're built with Python, or PHP, or Perl, or Java. Progressive enhancement is just going to be how they build anyways, but lots of the Node ecosystems move in this direction just because it's purely a better performance. Things like 11ty, which is a great static site generator, Remix, really started to bring this idea to the forefront with the React community. Then if you somehow got into the position where you have more than one framework, Astro actually supports them all, so you can compile them all but with an HTML first thing. That's really beautiful. This is how things should work.

I agree with this, because I've been saying this for a really long time, with our backend framework, Architect. Architect is a framework for building applications out of cloud functions. We have tens of thousands of users doing this. It's been around since around 2017. We always said just, return HTML, and progressively enhance it. People would look at me like I was a lizard for saying that, but I'm not. It's a very valid way to work. If your dynamic cloud function endpoint can return JSON, it could also return HTML. If it could, then why shouldn't it? That's a good question to ask. It's also a good question to ask like, what if our entire backend was cloud functions? We probably wouldn't have the performance bottlenecks that we have with servers and load balance systems in old school databases. That's exactly what Architect is and what we call a functional web app, or an FWA. We never really answered that question with Architect, and that's why Enhance came around.

Recap

Modern JS has problems, pretty big ones. One of the big problems is that we have a bunch of brittle incompatible niche ecosystems. Svelte component is not going to render inside of a React component unless you pull off some pretty big magic. Even then it's going to be very brittle. Non-standard syntaxes like templating libraries, or, worse, whole programming languages create very brittle output that is just hard to debug. That's a pretty big problem. That last problem is static, not dynamic. You get spinnerammer, and I think that's a terrible user experience for dynamic content, which the web should be really good at.

What Is Enhance?

The question we started asking the Architect project was, can our whole frontend just be pure standards-based HTML, CSS, and JS? Can we just progressively enhance it? The answer is yes. We never really packaged this up, but now we have. We want you to take these Legos and play with them. The answer is Enhance. Enhance is an HTML framework that's built on top of Architect, so you get all the dynamic goodness. Its premise is really simple, you start with HTML pages. You use generally available web standards. You progressively enhance working HTML if you need to. Because the baseline is built with cloud functions, this thing is going to scale up and down to whatever demand that you need. It uses a managed database on the backend, so you can have dynamic data without a spinner. The deployments are wickedly fast because it uses infrastructure as code, and so it's completely deterministic.

Demo

Let's see if my terminal is handy. There it is. I'm in my home directory. Jump over to my desktop. I'm going to run npm create @enhance. I'll give it a folder name of qconsf. Jump over to that folder, and we'll run npm install for the instructions. This takes a few seconds. It has its dependencies like any other project, and a few of them are pretty big. The main culprit here is AWS SDK, that's a chunky boy. Ironically, we don't need to ship AWS SDK to production, we just need it for the local sandbox. I kick up my web browser now, we could see I've loaded Enhance. Here it is. This is our homepage. Let's take a quick tour of where everything is and how everything's put together. Ok, so I'll kill my sandbox. Let's take a look at this. In the root, we have a few files. We have a prefs.arc, this is just to say I want to auto-reload. We've got a package JSON with one dependency, and that's this arc-plugin-enhance. We've got a public folder with some pretty typical static assets. We've got some prebaked CSS. We've got SVG files. The main event here is the app folder. Our app is pretty barren right now, we got app/pages, index.html. Say, hi from index, and some HTML. Let's start the web server on the second tab. If I pop over to my browser. Reload. You see, hi from index html. Let's add another page. This is similar to other frameworks out there like Jekyll, or Docusaurus, or the React one, Next. About, hi from about, let's add some HTML, I'll make this italicized. Pop over to my browser. I've got my index html. If I go to about, there's my about. It renders HTML. If I deeply nested this, so if I was like, hello/world.html. Like a folder here world, hello, world. Hi from hello/world. I go to my browser. We have hello/world. There we go, we can see that. This is getting a bit awkward, I have to use the Safari URL bar, and Apple, they're trying to get rid of the URL at the time.

Let's add a header. Now I'm going to add a folder called elements. In that folder, I'm going to call file, my-header.mjs. Now, Enhance is running in the backend. This is server-side rendered. I named this file mjs, because in Node, it prefers .mjs for ES modules. Now, export default function header. Enhance elements are just pure functions that accept two named arguments, HTML and state. You return an HTML string. HTML is a tag template literal so I can put whatever I want in here, but for now, I'm going to put some HTML for my header. It's good old semantic HTML here. We got nav. Nav is going to have some links. We had about. We've got hello world. We'll close this nav. This is a tag template literal, with some HTML in it. It's named header. Importantly, under app elements, it's in my-header. Now I'm going to go into these HTML files I created and I'm going to add my-header. It's going to work like you would think. If I add my header, it'll expand it appropriately in each of these places. Let's do that. I'll add to index. We'll add it to hello. If I pop over here. We'll see this is unstyled, and that's actually deliberate. Enhance ships with a utility CSS system and it does a reset by default. You can override it. You can bring Sass or whatever it is that people do these days. We think utility CSS has a lot of merits, so we built one in.

You can see if I click on these links, it's going to take me to those destinations. That's not that surprising. What is surprising is how this all works. We've got a little WebSocket code in here for the browser reload. If we look down here, you'll see my-header is expanded. There is no client-side JavaScript running. In fact, we could just go right ahead and disable JavaScript and see this still works. It's still snappy. It still does what it's supposed to do. All of this is expanding the web components, server-side, which is important. There's a custom element. There's the links. If I go to these other pages like home, same difference. That's really powerful and nice. We didn't need client-side JavaScript to render a custom element. We got some reuse. We have really clear templates. We're using just standards-based stuff right now. We've got standard, pure JavaScript, and standard HTML.

The next big trick for Enhance is to show how we do API routes. I'm going to add an API route for index. Similarly, to the file-based routes for pages, you can add API routes, as just names and have the map. I've got pages index html, let's add api/index.mjs. API routes can respond to HTTP verbs. Export async function get, would respond to an HTTP get request. Likewise, if I wanted to do like a post, export async function post. My form is posting to index, this would get invoked, this function right here. Let's do the get request first. The get request is going to return a bit of JSON. That JSON is going to have a key of stuff, and a value of an array with some scalar values. Very basic async function. Now, by itself, this doesn't mean much. I could make an XMLHTTPRequest to index right now, and if I had a accept header of application JSON, it would return that value.

I'm going to show you something even cooler. I'm going to duplicate my custom element, my-header, and call it my-debug. I'm going to get rid of all of this. I'm going to change this to a very trivial debugger. This is just a regular tag template literal. I can use the dollar sign syntax to do interpolation. I'm going to JSON.stringify state. You might remember, when I was first talking about this, I said all the elements are pure functions that receive this HTML function and state, and then they return an HTML value. This state will get prepopulated by this index. If I now add, my-debug, and we can look, hi from index html, there's nothing here. My-debug, say that. Socket didn't load, but there we go. Now we've got it. This is the debugger component outputting the state that got parsed in, that state got parsed through. If I made an XHR request, I would invoke that lambda function again in the backend, which means that we can do progressive enhancement with the values that are not interpolated with HTML. That's really nice. You can use that state to render stuff. We can use it to manipulate this page, or anything in our components. That's very handy. There's a whole lot more to show, Enhance works with forms. There's a whole utility CSS system built in. We have CRUD generators. We can deploy this to AWS.

The Key Concepts of Enhance

Some key concepts for Enhance that I'd like you to take away. It's file-based routing with just plain HTML. If you know HTML you know Enhance. It's not like HTML, it is HTML. You can reuse markup across HTML pages with custom elements. It's a pure function that returns the same thing every time. There's a built-in utility CSS system that's based on scales rather than absolute values. It's going to work with responsive web design without having to compile a whole bunch of different versions of that CSS. I think it clocks in at around 20 to 30 kilobytes. We actually just inline it in the head of the document, you don't even have to think about it. If you didn't want to use the CSS, because you don't like utility systems or whatever, it fully supports standards-based CSS. API routes are really nice. You don't have to manually wire any props. Because of the way that API routes work, it's very simple to progressively enhance this whole system with standard JavaScript, and no special syntax is required. Again, under the hood, this is a full stack dynamic web application. You don't need spinners to display dynamic data. You can just render it. Enhance brings together both frontend and backend concepts, but it's also a very HTML focused and standards-based focused. Our real driving force here is to bring some stability back to the ecosystem and just stop loading everything to JavaScript and hoping it's just going to work. This is our vision for the future. Try it out at enhance.dev.

Questions and Answers

Dunphy: One of the key features that developers love from single page apps is the virtual DOM, and the ability to control stateful interactions. What is the response from Enhance to developers who love or perhaps require building with these features?

LeRoux: It's a common way to approach building more interactive applications or more long-lived applications, maybe, apps that are going to have longer user sessions, sometimes you like to have all that functionality baked in at one key page. I actually don't think there's such a thing as a single page app. There's always a few pages, and then there's one main page. Nothing in Enhance is stopping you from embracing using any of the existing technologies out there to do that. If you want to embed a React app inside of it, you could do that. I think that's totally appropriate for some cases of application. I would encourage most people, though, to not start there. I would start with the simplest thing that could possibly work, and then build up to that and see what you could do with just vanilla JavaScript. I think people would be very pleasantly surprised to discover that it has advanced quite a bit in the last 10 years.

Dunphy: Is this why you think maybe Remix is really sparking a lot of innovation in this space. I think the founders, Ryan Florence, and Michael Jackson, very bullish on Rails, very bullish on web standards. This is something that they've tried to bring into that framework. Is this something that you see as more in line with what you're trying to do with Enhance, separate from other React frameworks like Next, and perhaps even Gatsby?

LeRoux: Gatsby is fine. Yes, I really admire what Ryan and Michael did with Remix and how fast they really executed on that. Now it's a part of Shopify. I think it's got a really great future ahead. Yes, we're very compatible, philosophically. A place where we differ is really in the transpile step. I don't think that's necessary anymore, and has bad tradeoffs. I think those tradeoffs are really important if you're building a business application. If you're building up a stack blog or something, and you don't mind your dependencies breaking, then that's cool. If you're a line of business application, you're processing customer orders, or have perhaps governance and issues around stability, you're going to want to stick as close to the metal and standards as you can. I think React is great. I think Remix is great. I think these tools serve a purpose. I think a lot of the reasons they came about no longer apply. I think it's time to revisit that.

Dunphy: It is curious that Shopify did acquire them. It reminds me of a meetup we had featuring Alex Russell, noted React critic, in San Francisco. One of the things he pointed out was that React is extremely complex and error prone, specifically with e-commerce apps. He asked this question, who is having problems with React in e-commerce apps? A number of folks raised their hand. One person in the Q&A, did ask a question specifically about this. I'm not working directly with e-commerce apps, so it was an interesting data point for me to explore a little more. I'm wondering if the purchase of Remix is actually going to solve some problems that Shopify was experiencing on their own because they are very much a React shop.

LeRoux: Very interesting, because I feel like they get a lot of data. They have a lot of users and a lot of downstream users of their marketplace. They've bet big on React and so they want more skin in that game, I think. I imagine it's in everybody's interest to improve it and continue to make it better. I'm not sure if it's complex or not. I think we make it complex. I think it actually is quite terse and beautiful, and really did move the state of the art forward for us. All credit to them and other frameworks like them like Svelte.

Dunphy: One of the things that makes React so popular is not just its feature set, but also its developer experience, with loosely held opinions and a rich ecosystem of tools to support virtually any use case. It's an extremely popular tool to build with. At the same time, we've seen criticisms of React, and specifically its performance, and just general overuse in the community where it's not required. Where do you think Enhance fits into the typical web developer stack these days? What is the sweet spot for Enhance?

LeRoux: I think it's a first-mile solution. When you're learning frontend, or you're beginning a new project, you're probably going to start with HTML, even if it's a React app. What better way to do that than to actually start with HTML and start building up your application from first principles as opposed to like, diving in and trying to npm install your way to success. We see a lot of people with failed builds in begin.com, because they add so many dependencies, and then when we work with them in support, often many of those dependencies they're not even using. They've got a lot of code in the hot path that's just sitting there for no good reason. Then, the more of a dependency you have, the more brittle the situation gets, and it's a House of Cards. For me, the sweet spot is when you're getting started. You may have to eject at some point. It's HTML, so you're going to have a good time doing that. It's not going to be hard. You already know HTML. Another thing that I like to bring up, people are like, what about the ecosystem? React's got a big ecosystem, so does the web. The web is actually arguably the bigger of the two. You wouldn't be able to do React if you didn't know HTML, CSS, and JavaScript. You've already got those skills. It's already there for you. It's a good place to begin. It's a good place to build up from. Because under the hood, it's a functional web app, which means it's been built with lambda functions. It's going to scale up like really far. You'll be able to build an application that can handle 10k requests per second, the first time you deploy it, and an application that talks to a real database, not a static.

 

See more presentations with transcripts

 

Recorded at:

Sep 07, 2023

BT