RailsConf 2021

Hotwire Demystified

Hotwire Demystified

by Jamie Gaskins

In the video titled 'Hotwire Demystified,' Jamie Gaskins discusses Hotwire, a framework introduced by Basecamp that simplifies modern web application development by sending HTML instead of JSON. The talk provides an explanation of Hotwire's key components and how they work together to streamline the development experience while minimizing the need for JavaScript.

The key points covered in the video include:

- Introduction to Hotwire: Hotwire stands for HTML Over The Wire and is presented as an alternative methodology for building web applications.

- Components of Hotwire: The three main components are:

- Turbo: Enhances page navigation speed and offers features like frames and streams that allow partial page updates without a full reload. Turbo frames act as UI components that can load and update independently, resulting in better performance.

- Stimulus: A companion framework that provides additional custom functionality, complementing Turbo's capabilities without changing the way it operates.

- Strata: Although not elaborated upon due to its unreleased status, it is mentioned as a potential bridge for building native mobile applications using web technologies.

- Turbo Frames in Detail: Gaskins explains how Turbo Frames diminish the need for entire page reloads, allowing more efficient server resource usage by only updating necessary parts (like cart item counts).

- Turbo Streams: Gaskins discusses how Turbo Streams can send only the changed content in responses, allowing for more efficient, selective updates to the webpage.

- Action Cable Integration: Integration with Action Cable enables real-time updates via WebSockets without the need for JavaScript coding, simplifying the process of maintaining UI state across client-server interactions.

- Trade-offs Between Hotwire and SPA Frameworks: The video concludes with a comparison of Hotwire and traditional single-page applications (SPAs), emphasizing differences in engineering effort, performance, and user experience.

Key examples include a product catalog application demonstrating Turbo Frames and Streams in action and comparisons to common practices in SPAs that reveal how Hotwire can streamline development while reducing client-side complexity.

In conclusion, Gaskins encourages attendees to consider Hotwire for simpler applications where reduced JavaScript and faster time-to-first-interaction are priorities, while recognizing that single-page applications may still be advantageous in specific scenarios.

00:00:05.660 Hi friends, my name is Jamie Gaskins, and today we're going to be talking about Hotwire.
00:00:10.740 By now, many of you have seen Basecamp's Hotwire announcement screencast, which shows how to create a real-time chat application with multiple chat rooms without writing a single line of JavaScript. By the end of this talk, my hope is that you'll understand some of the science behind the magic that was shown off in that screencast.
00:00:17.699 Here's our agenda for this session. We're going to go over some of the pieces of Hotwire and how they impact how you build your application. First, we're going to look at what Hotwire is. The folks at Basecamp call Hotwire an alternative approach to building modern web applications by sending HTML instead of JSON.
00:00:28.920 Hotwire is an acronym that stands for HTML Over The Wire. The Hotwire website lists Turbo, Stimulus, and Strada as its three main components. The first piece we're going to talk about is Turbo. Turbo provides the most basic building blocks for building Hotwire applications. Turbo used to be called Turbo Links, and its main purpose was to make web page navigations faster by preserving the rendering and JavaScript execution contexts. This way, the browser doesn't need to completely tear down and reconstruct those contexts every time you click a link.
00:00:55.800 That's still here in Turbo, and you still get that functionality for free just by loading the JavaScript payload. It also provides features called frames, streams, and Action Cable integration, all of which we'll dive into more deeply in a few minutes.
00:01:18.720 Even though Turbo provides a lot of functionality out of the box, your application may have some features that don't fit well into its capabilities. For those features, you can use Stimulus, the Stimulus framework, to provide some custom functionality to fill those gaps. We won’t get into detail about Stimulus because Hotwire doesn’t actually change how you work with Stimulus controllers inside your Rails app; you still use them the same way you did without Hotwire.
00:01:41.220 Strada is a bridge that lets you write native mobile apps using web technologies. The Basecamp team hasn’t elaborated on this much, but if I had to guess, it likely has an API similar to some of the things you'd find in the React Native ecosystem, such as native push notifications, integration with your photo library, or maybe access to other sensors and APIs available in native apps. We won’t be going over Strada during this talk because it hasn’t been released yet, and we don’t have much information about it.
00:02:10.200 Another topic we won’t cover is how to compose all of these concepts into a slick web app. For that information, you might want to check out any of the talks at RailsConf, listed in the order they appeared on the RailsConf website. We are going to be looking at how the things they cover in their talks work under the hood.
00:02:38.819 So that's a brief introduction. Now let's get to the fun part. The first thing we’re going to look at is Turbo Frames. Turbo Frames are the primary building block of Hotwire. Mechanically, you can think of them as chunks of your user interface—small blobs of HTML rendered into the resulting web page.
00:02:50.099 In some web frameworks, these may be called components, and in Rails, they’re often called partials. Turbo Frames are largely the same idea, and many times, partials are how you construct your Turbo Frames. Let's say you have an e-commerce application with a product catalog. The product catalog itself can be a Turbo Frame, and each product inside the catalog can also be a Turbo Frame. You can compose frames within other frames.
00:03:16.080 Tucked up here in the corner, we may have some secondary content, also considered Turbo Frames—in this case, our cart showing the number of items in it, along with some order notifications. But why would we do this if we have partials? What makes Turbo Frames so special?
00:03:39.840 Let's say we're on the product catalog page. When we hover over a product, an 'Add to Cart' button appears. After adding an item to the cart, we want two things to happen: first, we want the background of that particular product to turn green, so it indicates it's been added to the cart with the design language we have chosen. Second, we want the badge on the cart to update to reflect the new item count.
00:04:05.159 In a typical Rails app, when you click the 'Add to Cart' button, it sends a request to the server. The server makes some changes in the database and responds with a redirect, which may be to the same page we were on or to a pre-checkout page, or another entirely different page. For simplicity, let’s say we will return to the same product listing we were originally on. Now the browser makes a second request after the redirect to get updated content, and the server responds with the entire page.
00:04:46.860 However, the only things that actually changed were the product added and the cart item count. Sending the entire page back uses significant server resources and potentially transfers more bytes over the wire than necessary just to update the page. With Turbo Frames, you can easily get only those two updates; the rest of the page remains unchanged.
00:05:11.160 We will explore how that works as we proceed further into the talk because one more thing we need to cover is how to process Turbo Frames in the response. Another great feature with Turbo Frames is that it allows you to delay rendering until after the primary content loads.
00:05:28.740 For example, let's consider these secondary elements, such as the cart item count and notification count, which may take considerable time to compute. If the database tables they involve are massive, or if the queries are intricate, or perhaps they need to reach out to external services, we want to load the primary content as quickly as possible. Users load the product catalog to consider purchasing items, so we don’t want to keep customers waiting to make that purchase.
00:06:04.440 The secondary content can hold off loading until after the primary content is displayed. We can achieve this by replacing the cart item count and notification count badges with Turbo Frame elements that have an ID and a source attribute. The source attribute tells Turbo to fetch the frame content asynchronously after the page loads.
00:06:30.060 When the response returns, if there's a Turbo Frame element with the same ID in the response body, its contents get injected into the Turbo Frame element. You’ll notice that we explicitly close our Turbo Frame, even though it loads asynchronously with the source attribute. This allows you to include loading indicators or placeholder elements to make the UI feel more stable.
00:07:02.639 Keeping the layout consistent is essential. If secondary content loads asynchronously, it might shift elements around, causing poor user experience. But in our case, we're loading badges that don't have any layout implications, so as long as the HTML response contains a Turbo Frame element with the correct ID, Turbo will insert its contents.
00:07:35.580 Rails provides a helper method you can use if you’d rather not use an actual HTML tag. When we add this action in our cart controller, it will handle the request, referring to the partial we extracted the content from.
00:08:00.000 When we see this on page load for our cart, we see updates after that secondary request returns. The request behind the scenes looks similar to this: notice that we have the Turbo Frame header in the response, stating the cart item count. That’s the Turbo Frame we aim to populate.
00:08:21.660 The Turbo JavaScript library manages the request for this content and populates that header. This enables us to provide the correct Turbo Frame ID back in our response, and Rails uses this under the hood. There’s still more you can do with Turbo Frames, but we have a lot of ground to cover, so let's proceed to Turbo Streams.
00:08:45.540 Remember how we discussed sending over only the content that changed on the page? Turbo Streams are the concept in Hotwire that enables that functionality. This is the Turbo Frame for our cart item count after it has been rendered on the server.
00:09:00.319 Notice that its ID is 'cart item count', which is vital. When we set our response mime type to be a Turbo Stream response, we use this content type header. Our Turbo Stream payload looks like this: the target attribute matches the ID of the Turbo Frame we want to update. The action specifies what we are going to do with this Turbo Stream's contents.
00:09:22.320 In this case, we are saying we're going to update the target frame, meaning we will replace its contents with the new data. The Turbo Stream contains a template element, which tells Turbo the content inside is what we will perform the action with. Thus, we perform the action on the target frame with the template, replacing the old contents.
00:09:46.740 The Turbo Stream for the product index item is similar; it's rendering new markup for the item in the catalog. In this case, the action is 'replace', meaning we are not only replacing the contents but the Turbo Frame itself. If we use 'replace', we have to include a Turbo Frame element to maintain future updates.
00:10:12.500 We’ve covered the update and replace actions, but there are five actions Turbo Stream can perform out of the box. The 'update' action swaps out the contents of the target Turbo Frame with the stream's template contents. This is equivalent to setting the inner HTML property on the Turbo Frame element with JavaScript.
00:10:40.680 The 'replace' action swaps out the Turbo Frame itself, not just its contents. This can be compared to setting the outer HTML property of the Turbo Frame. The 'append' action adds the contents of the Turbo Stream's template to the end of the target frame, using the JavaScript 'element.append' method.
00:11:07.200 The 'prepend' action adds the contents of the Turbo Stream's template to the beginning of the target Turbo Frame, employing the JavaScript 'element.prepend' method. Finally, the 'remove' action deletes the target Turbo Frame from the page without needing a template.
00:11:40.680 We discussed updating both of these elements when adding an item to our cart. You can send as many Turbo Streams as you like in one Turbo Stream response, allowing your application to make all necessary changes in a single response. This eliminates the need for two requests to the server to update both the cart item count and the product index item.
00:12:02.700 Not all Turbo Frames specified here need to be visible on the page. You might have a Turbo Stream endpoint that updates various Turbo Frames across parts of your app. Next, we’ll explore how to send Turbo Streams across WebSockets using Action Cable.
00:12:30.360 To use Action Cable with Turbo Streams, you need to import Turbo Action Cable and Turbo Rails into your application.js file. If all you need is Turbo with Action Cable, these are the only JavaScript lines required. There's no further JavaScript coding necessary; you don’t need to define special Action Cable channels in JavaScript.
00:12:54.660 All of that is handled through HTML and server-side Ruby code. Then, in your HTML, you include a Turbo Cable Stream Source element to connect Turbo’s Action Cable with Turbo Streams. However, I wouldn’t recommend writing this element by hand; the Turbo Rails gem provides an Active Record integration that encodes stream names using base64 and securely signs them with your Rails secret key.
00:13:22.500 The browser simply needs to subscribe to that stream name, which you don’t want to manage manually in your views. Instead, the Turbo Rails gem provides a 'turbo stream from' helper to generate that Turbo Cable Stream Source element. It's very clean, allowing you to provide either a static stream name as a string or pass an Active Record model to it.
00:13:46.440 The Turbo Cable Stream functionality hinges on the sending side and the receiving side agreeing on names. Once you have multiple Turbo Frames and Turbo Cable Stream Source elements on your page, adding a Turbo Cable Stream Source element opens a WebSocket connection to your Action Cable endpoint and subscribes you to all those streams.
00:14:06.180 We didn't write any JavaScript for this; we merely rendered an HTML element. When we import Turbo Rails into our application.js, it creates a custom HTML element that knows how to behave when the Turbo Cable Stream Source element is added to the page. The browser handles its connection to Action Cable, automatically subscribing to the streams without any manual effort.
00:14:30.600 When the Turbo Cable Stream Source element is removed, its disconnected callback is triggered, unsubscribing from updates on that specific stream. For example, if a response filters out some products from the catalog, you are automatically unsubscribed from those updates.
00:14:54.480 Let's say you’re working on a feature where, if a customer has just purchased the last item of a specific product in your inventory, you want to update the catalog to indicate that it is no longer available. You can update all the products in that customer's cart by calling a single line of code.
00:15:17.340 What we’re doing is broadcasting an update to the product index item with the product ID while specifying the partial we want to render and the local variables needed inside that partial.
00:15:38.640 Next, we’ll examine the trade-offs between Hotwire and single-page apps built with front-end frameworks like React. The trade-offs will include the effort required to build each app, how latency affects them differently, performance on slow versus fast devices, and how the server communicates changes and the client responds.
00:16:01.320 The first trade-off involves engineering effort. There are many ways to analyze this, so we'll focus on a few key aspects for the sake of our talk. The first major factor is getting data from the database to the screen. We’ll use a refresher on how this works with Hotwire.
00:16:30.060 When we send a request from the browser to the server for the product catalog, the server responds with the content of that page. When adding an item to the cart, we only receive the HTML elements that need to be changed.
00:16:47.280 To understand the engineering effort involved, we need to explore the interactions of all relevant objects. The dependency graph illustrates most of the pieces; there’s more, including the layout template that manages the rendering of the cart item count, but we can’t fit everything on this slide.
00:17:21.780 As we render the initial page, the products controller index action fetches the products from the database using the product model. It renders those products into the products index template, which in turn utilizes a partial to include the Turbo Frame for that product. The product index template also renders the Turbo Cable Stream Source element needed to activate the Action Cable stream.
00:17:43.620 After this, our page renders. When a product is updated, perhaps due to order fulfillment that reduces inventory to zero, we may have a broadcast callback on the model, as recommended in the Turbo Rails gem.
00:18:10.260 This callback knows which template to render and which Action Cable stream to broadcast to, making it part of the dependency graph needed for displaying the products on the page. When someone adds an item to the cart, we insert or update the cart item using its model, and the model's broadcast callback knows to broadcast an update with the template's contents.
00:18:36.420 While this may sound complex, Rails conventions significantly simplify this. The app has multiple representations of a product—hypothetically, even while showing just the product catalog, it could be displayed on a detailed product page, a checkout page, and various other locations.
00:19:01.920 This can be simplified. For instance, when we previously broadcast updates to the product index for an Active Record model not used everywhere, we can utilize a simple Active Record callback declaration without specifying IDs. Rails will infer those IDs similarly to form helpers.
00:19:31.680 We also do not need to specify which partials to render to the Action Cable stream or even which stream it goes to, and we do not have to broadcast an update explicitly when performing actions.
00:19:54.240 Things that are straightforward in Rails tend to be very easy. However, when needing multiple representations, you must specify IDs, partials, and stream names to prevent rendering the wrong template when updating.
00:20:05.640 Now let's look at how this setup differs in a single-page app, like one built with React or other frameworks. The JavaScript component has numerous elements, creating tight coupling.
00:20:38.020 In our server-rendered app, there were numerous dependencies, but most flowed in one direction. In JavaScript apps, dependencies can spread more widely. In a typical React app using Redux, components dispatch actions that trigger the reducer to update the state. React then re-renders connected components to that updated state.
00:21:14.880 When your app first renders the product list component, it might not yet have the loaded products, prompting it to dispatch a 'fetch products' action. The reducer sends a request to the product index endpoint, retrieves products from the database, serializes them with the product serializer, and the data goes back to the reducer.
00:21:57.300 This results in another action to load the products, causing another reducer run that updates the Redux state. The product list component then re-renders with the new data. This entire process just to load data from the server can be cumbersome.
00:22:30.600 While there are conventions that can streamline the process, they don't eliminate the manual work that Rails handles easily. I would love to explain how the WebSocket updates function, but space on this slide is limited.
00:23:08.760 Another significant aspect is latency and how it impacts the application's performance. How you mitigate latency can make a difference between conversion rates and lost customers.
00:23:39.240 Latency can be further divided into key metrics depending on whether you measure from when a user opens a page versus how they navigate by clicking links. What you’re seeing here is a representation of a web app's load process. The browser connects to the server to secure the connection using TLS, sends an HTTP request, and the server generates a response based on content type.
00:24:14.640 Once the server response is sent back, the browser begins rendering. In reality, the process can be even more complex when factoring in load balancers, reverse proxies, and caching.
00:24:47.640 When sending HTML over the wire, the response generation involves fetching many records from the database, transforming them to HTML using ERB, Haml, or Slim templates before sending back to the browser. Until the rendering occurs, the user sees a blank screen.
00:25:10.800 For a single-page app, the generation step is shorter as it typically serves a static HTML page. However, this static page is not particularly useful until the JavaScript is loaded.
00:25:37.440 We also notice the content loading steps overlap. The browser can request JavaScript while still processing HTML, but that doesn't significantly impact the performance.
00:26:01.920 The steps for responding and executing JavaScript become longer. Single-page apps often send large JavaScript payloads, which can lead to delays in loading responses, especially for users with slow connections.
00:26:29.160 Additionally, once the content renders, until data is loaded, the user still experiences a blank screen. Another server request is often necessary for the product catalog.
00:26:51.720 Although a single-page app might show a loading indicator, when you initially load an HTML app that takes a while to render, the user just sees a blank screen.
00:27:09.760 In contrast, navigating within a Hotwire web app doesn't require loading all assets again, but you must request the server for every navigation. Turbo does implement a caching layer, so if there’s a cached version of a page, it will render it while loading fresh content in the background.
00:27:30.000 Turbo also displays a loading progress bar, providing users a visual cue that page transitions are taking place. It adds a busy attribute to Turbo Frames to design loading indicators.
00:28:10.800 When a single-page app navigates, the experience may differ depending on whether your JavaScript build employs a technique called code splitting. This technique allows you to load smaller portions of JavaScript, reducing initial load times.
00:28:37.800 Yet, if your single-page app loads everything in bulk, navigating might be fast since it utilizes existing UI components without invoking the HTML parse.
00:29:06.720 However, when data fetching is required, this complicates things. The size of JavaScript payloads for single-page apps can also vary considerably; it's common to see one megabyte or larger payloads, leading to slower execution on older devices.
00:29:36.180 A standard React app can weigh in at more than two and a half times the size of a Hotwire application, which requires significantly less JavaScript to achieve similar functionalities.
00:30:02.640 This can result in the browser needing to evaluate three to six megabytes of JavaScript, leading to performance issues, especially for mobile users.
00:30:27.960 Thus, even when using a powerful machine, we must remember that users on slower devices may struggle with JavaScript execution times, especially if the applications exceed manageable sizes.
00:30:56.220 Hotwire operates under the premise that much of the JavaScript will be handled for you, keeping payload sizes small and making the experience more seamless for users.
00:31:23.520 Keep in mind that these are just trade-offs. Each approach has its pros and cons, and determining which best meets your needs depends on what you prioritize.
00:31:46.680 If your budget is tight and you're already rendering HTML, and your team lacks extensive JavaScript experience, Hotwire may be the better choice. However, if you already have a solid API in place, and interaction latency matters most after initial loading, you might prefer a single-page app.
00:32:10.320 These considerations should be factored into your choice of framework.
00:32:32.640 If you’re attending RailsConf virtually, feel free to reach out to me in the Hotwire Demystified channel on the RailsConf Discord. You can follow me on Twitter at the handle shown in the bottom right corner of this page.
00:33:01.680 And of course, as we all know, like, subscribe, and ring the bell for notifications. Just kidding—wear a mask and enjoy the rest of RailsConf 2021. Thank you for watching, and I hope to chat with you all on Discord.