How Phoenix LiveReload works behind the scene?

Demystifying the magic behind the inner workings of LiveReload

Phoenix provides a good developer experience by auto reloading the webpage whenever you change the source code of your project in the development environment. Whether you are working on CSS/JS files or on your elixir module, providing an updated webpage before you switch your window from your code editor to browser is quite handy.

In this post, we will look into how this is working behind the scenes. Phoenix LiveReload as this feature is called is made possible by using Phoenix Channels, a built-in feature in Phoenix framework. Yes, this is the same one that is powering Phoenix LiveView, an actively developed new feature for Phoenix that is making javascript code redundant for many use cases.

To better follow this post, I suggest that you create a new Phoenix project using mix phx.new hello and follow the code explanations by actually opening the relevant files in your project.

Live Reload functionality is made possible by these 4 components:

  1. Phoenix Channel (bundled with Phoenix framework)
  2. File System https://github.com/falood/file_system
  3. Phoenix LiveReload Plug
  4. A small javascript helper present in Phoenix LiveReload

What is Phoenix Channel?

If you are already familiar with Phoenix Channel, you can skip this section. You won’t lose anything related to LiveReload. Phoenix Channel makes it possible to create persistent connection between client (browser) and the web server (cowboy). Traditionally, automatic updates to a webpage from the web server had been challenging as the HTTP protocol is client centric, meaning, it’s always the client who makes the request and the server responds to it. Once the server responds to a client, it cannot later decide to send any additional data to the client on its own at a later time without a new request originating from the client. So every response from the server needs a prior request from the client.

Phoenix Channel makes use of web sockets protocol (similar to http protocol) which is becoming the new standard for making 2 way persistent connection between the browser and the server. (Aside, Phoenix Channel is not directly tied with web sockets. In fact, Phoenix calls web sockets as transport layer. There are other transport layers such as LongPoll and these are beyond the scope of this post. It suffices to understand Phoenix Channel makes it possible for the server to communicate with a browser with which it had earlier make an handshake.)

Exploration Step 1

In a brand new phoenix project created using mix phx.new command, you can notice that dev.exs has the following code:

1
2
3
4
5
config :hello, HelloWeb.Endpoint,
  http: [port: 4000],
  debug_errors: true,

  code_reloader: true,

The code_reloader config in the dev.exs is read by Phoenix framework and it creates a boolean variable code_reloading? which is available in the endpoint.ex file in your project.

The following code is present in the endpoint.ex

1
2
3
4
5
6
if code_reloading? do
  socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
  plug Phoenix.LiveReloader
  plug Phoenix.CodeReloader
  plug Phoenix.Ecto.CheckRepoStatus, otp_app: :hello
end

Endpoint is responsible for serving all web request to your phoenix project. So the above code is conditionally injecting several plugs and Phoenix socket configuration (used by Phoenix Channel) into this endpoint.ex file.

1
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket

The above code defines a path /phoenix/live_reload/socket and maps it to a module Phoenix.LiveReloader.Socket. Any request coming to this path will initiate a persistent connection between the browser and the server using Phoenix Channels using the module Phoenix.LiveReloader.Socket.

Exploration Step 2

Now, let’s go http://localhost:4000 after starting Phoenix server using mix phx.server. Looking at the html source code in the browser, you can notice the following iframe code injected before the </body> tag:

1
<iframe src="/phoenix/live_reload/frame" style="display: none;"></iframe>

This iframe code is not from your phoenix project but got injected from Phoenix LiveReload library. The content of this iframe is an html document containing just a single<script> tag which is truncated below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
// first part
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof
...

// second part
var socket = new Phoenix.Socket("/phoenix/live_reload/socket");
var interval = 100;

// third and final part
...
socket.connect();
var chan = socket.channel('phoenix:live_reload', {})
chan.on('assets_change', function(msg) {
  var reloadStrategy = reloadStrategies[msg.asset_type] || reloadStrategies.page;
  setTimeout(function(){ reloadStrategy(chan); }, interval);
});
chan.join();

The first part of the javascript that I have truncated above is the minified version of phoenix.js from phoenix framework. This part of the js provides the Phoenix channel js client library.

The second part of the script configures the socket path using the js client Phoenix.Socket (provided in the first part above).

1
2
var socket = new Phoenix.Socket("/phoenix/live_reload/socket");
var interval = 100;

Note the channel connection is made to the path /phoenix/live_reload/socket and if you remember correctly, this is the path added in endpoint.ex conditionally if code_reloading? is true.

The third part of the js makes the actual socket connection and join the channel phownix:live_reload and starts listening on messages for the event assets_change. So whenever the Phoenix Channel server triggers the event assets_change and sends a message along with it, the javascript in the iframe will listen to it and respond.

So what do we have so far?

We have a regular html generated from your project with an additional iframe injected by Phoenix LiveReload library. This iframe contains an html document with a <script> tag which makes a phoenix channel connection to your application server running on localhost:4000 at the path /phoenix/live_reload/socket. This can be verified by looking at the network tab for websocket connection in your browser’s developer tools.

Now, when you make changes to your css, or js or any of your elixir module, the File System library which is included as a dependency library to Phoenix LiveReload notifies the LiveReload channel about the files that got changed. Phoenix LiveReload them sends a message to the browser with the type of file that got changed. Remember, the actual client that initiated the Phoenix Channel connection is the iframe inside the project’s main html page. So the message from the server about the file change is received by the iframe and not by the parent html document.

An example message from Phoenix Channel

Now, lets look into a what kind of message gets transmitted from the Phoenix Channel server and how the js client reacts to it. Open your developer tools in your browser and in the network tab, select “Preserve log” and filter the network traffic to show only websocket connection by selecting “WS”. If you then make changes to app.css file in your project, you will see a message in your websocket connection as in the screenshot below.

Live Reload network tab traffic

If you notice carefully, there are two messages: {asset_type: "js"} followed by {asset_type: "css"}. The reason for this is because of webpack bundler used by Phoenix to compile assets. All asset files (css, js, images, fonts) are watched by webpack server. The css file that I changed was app.css. This css is included in the js file app.js (as in the default phoenix project). Webpack compiles a new version of js and css files after every change to these files. Hence, even though I changed a css file (app.css), Phoenix server is reporting two events, one for the app.js file and another for the app.css both of which are emitted by Webpack bundler.

Let’s take the message {assert_type: "js"} and see how our js client in iframe responds to this.

The js client code responsible for handling the event and message is below:

1
2
3
4
chan.on('assets_change', function(msg) {
  var reloadStrategy = reloadStrategies[msg.asset_type] || reloadStrategies.page;
  setTimeout(function(){ reloadStrategy(chan); }, interval);
});

msg in the above function is the javascript object received as message {asset_type: "js"}.

Based on the value of asset_type, we find the relevant reloadStrategy. Reload strategy is a js function which when called does the minimal changes required for refreshing the page. There are two strategies present in Phoenix LiveReload: css and page. Since the asset_type that got changed in js in our case, page reload strategy is called.

Let’s look into the page reload strategy function and what it does:

1
2
3
4
var pageStrategy = function(chan){
  chan.off('assets_change');
  window.top.location.reload();
};

window refers to the iframe document which is receiving the channel messages while window.top refers to the top most parent document which is your actual phoenix project page that is embedding this iframe. A call to location.reload on window.top then reloads your webpage on browser and there ends our Phoenix LiveReload magic!

A page got refreshed once you made a change to assets without you having to manually refresh the browser.

Simple isn’t it?

Now, I could go on to explain several other nitty gritties of Phoenix LiveReload (and there are still a lot in this tiny library) but I think it’s best to leave you with a few exercises so you have the fun in finding the answers.

  1. Where in the request-response cycle does Phoenix LiveReload inject the iframe content in the response? And which code is responsible for doing this?
  2. How does Phoenix LiveReload ensure that this iframe is not recursively included in the HTML content of the iframe but only in the main html document?
  3. In the example shown in this post, a change in app.css triggers a {asset_type: "js"} and {asset_type: "css"} message. Can you think of a scenario where only css message is triggered?

Feel free to post your answers on twitter by tagging me. I will be happy to check the answers and clarify any questions. Avoid posting the answers here as comments as it might deprieve the joy of findings the answers for other readers ;-)

COVID-19 relief fund

If you have made so far and found the post interesting, please consider donating to the COVID-19 relief fund that I am collecting: https://gumroad.com/l/EEZzq or buy my books here: http://bit.ly/phoenixio 100% of all sales goes to helping people affected by COVID-19 financially. These include daily wagers, cab drivers and self-employed small business owners.


comments powered by Disqus