Video Summary and Transcription
In this Talk, the speaker discusses the concept of building JavaScript optional applications, focusing on the use of tools like React server components, Next.js, remix, React Router, Astro, SolidStarts, and Weld. They explore various aspects such as building eCommerce apps, pagination, adding items to cart without JavaScript, and implementing features like card previews using HTML and CSS. The speaker also highlights the trade-offs and considerations when disabling JavaScript, maintaining cart items, and combining old and new ways of building applications.
1. Building JavaScript Optional Apps
In this talk, I will share my findings on building react applications that can work without JavaScript. While highly interactive applications heavily rely on JavaScript, there are many applications that use JavaScript for cosmetic purposes. Tools like React server components, Next.js, remix, React Router, Astro, SolidStarts, and Weld are addressing the challenge of JavaScript-free experiences. Although only a small percentage of users disable JavaScript, the impact on application performance, specifically time to interactive (TTI), is significant.
Great to see so many faces here. And I guess let's jump right in. So the idea for this talk comes from the project that we worked on, which was an experiment to get an answer to the following question. Can we build react applications that could also work without JavaScript? And for lack of a better name, I started calling such applications JavaScript optional. And today I'm going to share with you my findings.
But first, let's talk about web without JavaScript. Some of you may have tried doing this in a browser so you know what I'm about to show you. But for those of you who haven't, I can summarize the experience of browsing web without JavaScript in one slide. Basically, it looks like this. There is a small number of applications that retain some degree of functionality. But in most cases, you will get either an application skeleton that looks like it's loading but it never will, or a simple message asking you to turn the JavaScript back on. And that makes sense, right? Because we know that highly interactive applications come part and parcel with JavaScript. So, without JavaScript, they simply break.
But not all websites are created equal. And while we have a large number of websites that have JavaScript as an intrinsic part for providing their core features, there is an even larger number of applications that use JavaScript to enhance user experience, right? To show more user-friendly input fields, add some micro interactions, things like that. But essentially such JavaScript serves a cosmetic purpose. And so it shouldn't be difficult to imagine that such applications could still work even when JavaScript is off, at least in theory. In practice though, it almost never happens because building applications for JavaScript optional approach requires an intent. But the best we usually do is put a no-script tag with a message and call it a day.
Now, all of that might sound irrelevant but I began to notice that tools and frameworks that we're using are starting to address that. And not only talk about this problem, but also give us tools that facilitate the development of JavaScript optional apps. So starting with latest React and Next.js that uses it. They give us React server components and server actions which allow us to move data fetching and data mutation to the server. Now, this is very important because that ability will become key to unlocking JavaScript-free experiences. Then there is also remix and applications build on top of React Router that use latest data router. They have slightly different abstractions, right? They use loaders and actions but essentially they achieve the same goal, fetching and mutating data on a server. And then even if we look outside of React ecosystem, we'll discover tools like Astro, SolidStarts, Weld, maybe even more that begin to address this problem of JavaScript-free experiences in one way or another. So that brings a question, right? Why is everybody talking about this all of a sudden? And if we look at the statistics of users who actively disable JavaScript in their browser, we're going to discover that the number is rather small, right? It's about 0.2% of all web traffic. But what matters a lot more is how this JavaScript optional approach affects application performance and a metric called TTI in particular, right? TTI, or time to interactive in layman's terms is the amount of time it takes from seeing a button to being able to click on it, because, you know, in React it doesn't happen instantly. We first need to download, parse, and execute the JavaScript bundle.
2. Building JavaScript Optional eCommerce Apps
If we build our applications to be usable even before any JavaScript is ready, we effectively reduce the delay to zero. Let's talk about building an eCommerce application using React server components and server actions.
And if we have a large JavaScript bundle and a slow network, then we have a problem, because this delay can extend into multiple seconds, and we see the users leave the website. But if we build our applications in a way that lets them be usable even before any JavaScript is ready, we effectively reduce this delay down to zero, which turns out to be very effective.
And there is a lot more to be said about the reasons for JavaScript optional approach, but in the interest of time, let's talk about a more interesting part and see how we can do it. So, to demonstrate that, we're going to build an eCommerce application. And for code samples, I'm going to use React server components and server actions. But it has to be said that all the techniques that I'm going to show you will work just as well with Remix, and in some cases, the solutions will be even simpler.
3. Building JavaScript Optional Pagination
To make an application work without JavaScript, we can employ graceful degradation and move local state into the URL. This allows users without JavaScript to have a more traditional style pagination, while users with JavaScript can enjoy a full fluid experience.
So where do we begin? Well, first, we begin by displaying a list of products. Now, if we have a very small list of products, we can simply fetch all of that on a server using a React server component in this example and just send a pre-rendered list back to the client. You know, this way is going to be available whether JavaScript is turned on or not. But this is very simple.
In a more likely scenario, we're going to need some sort of pagination. And we're going to add, for this example, infinite scroll pagination. So, to make it work, instead of pre-fetching all data on the server, we're going to pre-fetch only the first page of the results. And then on the client side, we'll use local state to keep track of the page that we need to load and an intersection observer that will fire a synchronous request to fetch the next page of results whenever the user is about to scroll to the very bottom. And that works great, except the moments we turn off JavaScript, users will scroll down to the bottom of the page and nothing will happen. Because when JavaScript is not available, we don't have access to state hook, any of the JavaScript APIs, and we can't even make a synchronous request.
So, to make it work without JavaScript, we're going to use a couple of techniques. So, first, we're going to employ a technique called graceful degradation, which means that for every more interactive user interface element that absolutely cannot work with our JavaScript, we're going to provide a less interactive fallback UI that can. And, in our case, it's as simple as falling back to a more traditional list of anchors for the pagination. Now, secondly, we're going to move local state into the URL, because we still need to keep track of the page that we want to load. And since we cannot use local state for that, we're going to lift it and put it in the search params of the URL stream. So let's see the entire solution. So when I render the list of products below, I'll just add a section that renders anchors to link into different pages of results. Now, if we don't want to show this UI to every user, but only to users who disabled JavaScript on a browser, we can do so by wrapping it into a no script tag. Now all we need to do is on the server side inside of the server component, instead of prefetching the first page, we're going to parse it from search params and that's the entire solution. So, then for the users who do have JavaScript enabled, they can enjoy full fluid experience with loading states and data being fetched and appended automatically. For users who don't have JavaScript, they get none of that, but at the very least, they have a more traditional style pagination that allows them to use the application.
4. Adding Items to Cart without JavaScript
To add items to the cart without JavaScript, we shift the responsibility of managing the cart from the client to the server. By using native browser form submissions, we can send the product ID to the server. The server action will parse the form values and update the user's cart. Users with JavaScript will enjoy a full experience, while those without JavaScript will still be able to add items to the cart.
Okay. Next let's see how we can add items to cart. There is multiple ways we can go about this, but a typical implementation looks sort of like this. We have a button that once clicked is going to add this item to somewhere in a global state of the application, maybe even to a local storage to make sure that it can survive page reloads. And once again, without JavaScript, this approach simply will not work. And in this case, to make it work, we are going to go with a completely different solution.
So, first of all, we're going to shift responsibility of managing the cart from the client to the server. So, instead of having cart in the local storage of user's browser, we're going to push it into the cart on the end user session on the server side. Secondly, we still need to have a way to send the product ID that we want to be added to the server. And for that, we're going to use native browser form submissions. So, to show you how it can work, first, we'll remove all the JavaScript APIs that we don't have access to. Secondly, we'll turn the button into a submit button and put it inside of the form. The form is going to submit to AddToCart action which we're going to see in a second. And finally, we'll add hidden input fields with the product ID because forms need inputs to send data to the server. Now, on the server, inside of our server action, all we need to do is parse this value from the submitted form values and then use it to update cart of on the user session. And with this change, once again, let me try again, the animation is playing. Yes. So, yeah, the users who do have JavaScript available, they will still enjoy full experience with loading states and toast notifications. Users who don't have JavaScript available will not see any of these nice features, but at the very least, the functionality will work and the item will be added to a cart. Now what's important to highlight here is that since we're not using form submissions directly but through a server action abstraction, we can get the best of both worlds, meaning that when we don't have JavaScript available, the browser is going to submit form to the server, get the entire HTML back as a response. But when we do have JavaScript available, the abstraction or server action is going to intercept this request for us and fire in a synchronous request only getting back the React server component that actually got updated.
5. Building Card Preview Feature
Next, let's build the card preview feature. We can implement components like popovers, model dialogues, context menus, simple carousels, and accordions using pure HTML and CSS.
Okay. Next I want to build this feature called card preview. So when we hover or click on the card icon, we're going to show a little popover with a preview of items in a card. And once again, a typical implementation would rely on the visibility flag that we would just toggle by clicking on the button and it would conditionally render the popover content. The same story here. Without JavaScript, this will not work. But this is a good place to consider whether we need JavaScript at all. Because these days a lot of components that previously required JavaScript to function now can be implemented completely without it. So things like popovers, model dialogues, context menus, simple carousels, accordions, all of these elements can be built by using pure HTML and CSS.
6. Leveraging HTML and CSS for Popovers
We leverage HTML and CSS more by using the newer HTML popover APIs. By giving IDs to the popover div and the button, we can link them semantically. Using the HTML anchor API, we can anchor the popover to the button. With some CSS, we can align and animate the component, making it work regardless of JavaScript availability.
So that's what we're going to do here. We're going to leverage HTML and CSS more. For this particular example I'm going to use one of the newer HTML popover APIs. So I'll get rid of the JavaScript, give IDs to the popover div and the button, so they can reference each other, and then we'll mark our popover div as a popover and give it as an ID to the button as a popover target. Now this is going to link them semantically, so the button is going to toggle the popover visibility, but they are not going to be visually aligned. So to fix that, we can also use HTML anchor API that will tell this popover to be anchored to the button that toggles it. And we can even sprinkle some CSS to align it better and maybe add some animations. So in the end, we get this component that looks and works exactly the same whether we have JavaScript available or not.
7. Checkout and JavaScript Dependency
Finally, let's talk about checkout. Checkout usually consists of two steps: collecting shipping details and processing payment. While we won't cover shipping details, we'll focus on payment processing. We often rely on external services, like Stripe, that require JavaScript. To handle this, we can gracefully disable UI elements that can't function without JavaScript. We can use a no script tag to hide elements and provide alternative UI elements to users without JavaScript. By understanding what's possible without JavaScript and leveraging it where not needed, we can create better user experiences and gain a competitive advantage. Consider exploring JavaScript optional applications to expand your skills as a developer.
Finally, let's talk about checkout. Now, checkout usually consists of two steps. We want to collect shipping details and then process the payment. Now I'm not going to talk about shipping details because it is a simple form and we already know that we can leverage native browser form submissions to deal with forms. So let's look at the payment processing instead. For this demo, I used Stripe integration for payment processing and a simplified version of it looks kind of like this, where we click on a button, it triggers a call to Stripe JavaScript SDK and it kind of takes over from there. Right? It's handled on the Stripe site. And here we find ourselves in this interesting situation where we rely on external service that is not designed to work without JavaScript, and there is nothing we can do about this. And it's important to acknowledge that situations like this will inevitably happen and the best thing we can do here is to gracefully disable parts of the UI that absolutely cannot work without JavaScript.
So this turns out to be quite an interesting challenge in itself because you want to disable something conditionally without using JavaScript. So instead, we're going to use a slightly different approach. I'm going to define this require JS class that will hide the element from the user. Now, we want this class to be applied only when JavaScript is not available. So we'll put this as a definition inside of a no script tag. An alternative way of doing that would be to use one of the newer media queries scripting none, but this one doesn't have good browser support yet. And so with this, all we need to do is give the class name to our pay button. And when the JavaScript is not available, it will simply be hidden.
Now, it's not a good idea to just hide UI elements from the users without letting them know what happens. So to make it work, we're going to add another no script tag next to it. And we'll put a disabled button inside. And in this example, I'm using title attribute to let the user know what's happening. And in the end, we get very different experience. But at the very least, people who don't have JavaScript available can proceed all the way to the checkout step, and they are notified that, you know, you reached the end. Now, it's time to get the JavaScript. So to recap, here are all the different techniques that I covered today. But if you think about them, they essentially boil down to the central idea of this talk, which is to know what's possible without JavaScript and don't use JavaScript where JavaScript isn't needed. Because it's very common to see these days that people who get into web development, they jump straight into frameworks and libraries, completely skipping the fundamentals. And knowing them, and knowing them well, will give you a big competitive advantage. And so to circle back to the why question we looked at in the beginning, you also may want to give JavaScript optional applications a try just to learn something new and hopefully become a better developer. And that's everything I have for you.
Disabling JS and Trade-offs
The 0.2% of web traffic consists of users who disable JavaScript due to privacy concerns or browsers disabling it for them. Slow networks and certain devices, like Chrome on Android, can also disable JavaScript. Accessibility repercussions of JS optional solutions can be addressed using different area attributes. However, some solutions may sacrifice accessibility and performance. Trade-offs between JS-free solutions and browser support should prioritize the majority of users on modern browsers.
Thank you so much. Okay. So the first question we have is why would users disable JS? And I think we're missing a mic. We can share. Sharing is caring. I have the lavalier. Oh, perfect. There we go. Sorry, I got distracted. What was the question? The question is why would users disable JS? Yeah, well, I think a lot of I mentioned the 0.2% of the web traffic and I think it's a lot of users with privacy concerns or using browsers that disable JavaScript for them. But it often happens that the JavaScript is not available and it's not the user's fault. For example, you can be on a very slow network and some devices, like Chrome browser on Android devices, if it detects slow network, it will just disable JavaScript for you. You'll end up experiencing what I showed, a completely broken application and not really knowing why it happens. Good.
The next one is any thoughts on accessibility repercussions of the JS optional solutions? Example, screen readers announce forms differently than stand alone buttons. I think you can get around this by using different area attributes. Of course, it's not just not all solutions will work well with this. And, for example, I mentioned some UI elements that you can build without using the JavaScript, but in the end, you have to pay attention because there's a lot of solutions that achieve that goal. They can work without JavaScript, but they're just horrible both from accessibility, from the performance standpoint, so you kind of need to know what you're doing, but to the The main question I would say, yeah, you can get around these problems with the forms by using area attributes. And, if you have any follow up questions, you can tweet Konstantin, find him on LinkedIn. Yes. And he's happy to help you with the solutions.
The next one we have is how do you do trade-offs between using solutions that don't require JS and browser support of the features? These rely on, example, popover API not supported on Firefox. Yeah, it's a good question. I mean, the popover API mentions specifically because it's just a good idea to keep an eye on the newer things, so I didn't want to show the boring CSS only approach. But it can also be done without the popover API. And, yeah, I mean, you shouldn't really sacrifice the experience of the majority of users who will be using modern browsers with modern features for the sake of a smaller number of users, because the ratio is very skewed, as we've seen. So, yeah. Sounds good. Firefox.
Maintaining Cart Items and Server Components
If a user builds a cart without JS and then turns on JS to pay, the cart items can be maintained by having the cart itself on the server side in the user's session. This approach does not affect performance for users who don't disable JavaScript. No-script tags are still handled by the browser if JS is enabled, but the required JS class would be ignored. Using non-JS implementation can be better for applications with a lot of traffic, such as the New York Times, where even interactive articles work without JavaScript. There are Node.js approaches, like static site generation with Next.js or Remix, that work completely client-side. Server components differ from historical server-side UI apps, as there is an oscillation between past and current ideas on application development.
Next one is, if a user builds a cart without JS and then turns on JS to pay, how do you maintain the cart items? Well, if you have the cart itself on a server side in the user's session, then it doesn't matter if the user has JavaScript enabled or not, because, you know, it always lives on the server side, and as long as the user is authenticated and the current checkout session is linked to them, it will be no issue. Sounds good.
How does this approach affect performance for users who don't disable JavaScript? Well, it depends on the techniques that you use, but generally it doesn't really hurt the performance. So, for example, the things I showed with the form submissions, like, you get both, the best of both worlds. So you get the fallback to a more traditional form submission, but it doesn't ruin the experience for users who have JavaScript. So they still get all the benefits. I think the main trade-off here is that as a developer, you need to think about this more and sometimes the implementation turns out to be, it takes more time to complete, but from the user perspective, I think it's all good.
Same experience. Next one is, are no-script tags still handled by the browser if JS is enabled? Or would the required JS class still exist for a user with JS enabled? I'm not quite sure I understand the question. Do we want a follow-up on the question? Do we know who the question came from? Maybe one of our, yes. Yes. Yes. Would the data be sent to the user with JS enabled? Well, the data itself would be still included in the page payload. It's just the browser when it processes the CSS. It would go through the style sheet, but ignore this part inside of the noscript tag if JavaScript is available. Thank you. Thank you for the follow-up. The next one we have, sorry, the screen just refreshed, let's give it a moment. I'm seeing some of the questions we already addressed. We're having a little bit of redundancy. Can you give an example that using non-JS implementation is better than using JS? I think we may have covered. Well, probably this approach makes the most sense for the applications that have a lot of traffic, they have a lot of users, so the impact on the 0.2% is actually significant. So I think New York Times, just to give like a very specific example, is very good at this because even their more interactive articles that use animated SVG visuals to support the idea, they still work without JavaScript enabled. On the flip side, there is a website like Medium where you can't even load the home page without JavaScript, so I think, well, the simple answer is, I guess, a blog or a website giving you simple content should not require JavaScript for such a simple functionality.
Gotcha. And then the next one is this relies on server-side rendering. Is there a Node.js approach that works completely client-side? For example, render JSX to static HTML as a part of the build? Yes. If you don't need any data mutation or server at all, you can just use static site generation, static export with Next.js or Remix or even Gatsby, and it will still work. Next one is how do these server components differ from other historical apps that ran UI on the server side? Seems like we're slingshotting back. Well, it does seem that way, but there is a bit of an oscillation between the ideas that we had in the past on how we build applications and how we do it now.
Combining Old and New Ways
Eventually, the old and new ways of building applications can be combined seamlessly. Developers who jumped straight into frameworks without mastering the fundamentals can identify knowledge gaps by following the state of HTML and CSS. Exploring CSS-only code pens on CodePen can also be helpful, but caution is advised to avoid excessive reliance on CSS-only approaches.
But I think eventually we're going to the point where you can combine the old ways of doing applications and the new ways without breaking stride. So, it becomes significantly simpler to combine them.
And this one says, any tips on identifying gaps in knowledge for those developers that jumped straight into a framework without mastering the fundamentals? That's a very good question. I would recommend, well, for example, for HTML and CSS, you can follow the state of HTML and CSS. There is usually a mention of all the new IPIs that browsers release that not many people know about. For me, it's also a nice exercise to explore CSS-only code pens, on CodePen, where as I mentioned, people try to build different components by just using CSS. You have to be careful with this, once again, because sometimes they go way too hard on this, but it's a good place to look at. Go hard or go home.
Comments