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.