Refresh the browser when a text appears on the terminal

How to save development time with event-driven live reloading

7 mins

Rebuild and reload

An unoptimized webdev workflow usually repeats the following pattern:

  1. Save the file
  2. Switch to the terminal
  3. Ctrl-c
  4. Up arrow
  5. Enter
  6. Wait for the app to restart
  7. Switch to the browser
  8. Reload

This workflow has two distinct steps: 2-5 is about restarting the application while 6-8 reloads the browser. Notice that only the first step has any real meaning, while all the others are boilerplate.

Every developer can do these steps by heart as this is what we do the most. It might take only 2 seconds but over time this adds up. And not only the time it takes is a problem but also the mental energy to switch from “developer mode” to “restart mode”. I’ve found that automating these steps increases my productivity way more than what the saved time would mean. Because of this, I always keep an eye on tools that help minimize manual work.

This article explains the following piece of code, that watches for a given string to appear on the console and reloads the browser:

<command> | tee >(grep --line-buffered done > >(npx livereload <(cat) -ee ' '))

Integrated development tools

Some tools are sophisticated enough to do both steps, rebuilding and refreshing, for you. For example, Webpack watches the files and whenever it detects a change it runs an incremental build and reloads the page. This takes care of all the steps after the save.

See how it works when running npx webpack-dev-server and changing something:

You can see that whenever there is a change, Webpack automatically picks it up, rebuilds the code, then reloads the browser. This functionality is integrated into the devserver and you don’t need to configure anything. It just works.

Webpack dev server handles the build and the reloadwebpack-dev-serverwebpack-dev-serverbuildbuildbrowserbrowserchangerebuildreloadreload

The main drawback of Webpack is that it’s for a specific type of project. It boosts productivity for a complex frontend webapp but it can not be used for a backend function or a Lua script.

Shameless plug
We write articles like this regularly. Join our mailing list and let's keep in touch.

Standalone tools

There are tools to automate one or the other of the two processes that are not tied to a specific type of project. One of my favorites is nodemon. It watches files and restarts the app whenever it detect a change. I even use it with Webpack sometimes, especially when I’m working on the webpack.config.js, as that needs a full restart.

To automate the other part, there is node-livereload that works similar to nodemon: it watches files and sends a signal when it detects a change. On the browser-side, either a Javascript snippet or a browser extension then receives that and reloads the page.

The best thing about these tools is that they are not dependent on a narrow set of technologies. Whenever there is a need to run something when a file is changed, nodemon can handle it. Similarly, whenever a browser refresh is needed, node-livereload is the tool.

The neatest thing I used nodemon for was to watch a Lua file and synchronize it with an ESP8266 microcontroller. I modified a line, hit save, and after a few seconds the text on the small LCD panel next to me changed. It made an otherwise involved process fully automated and suddenly it became fun to write IoT applications.

But there is a catch.

When one tool does the restarting and the other the reloading of the browser, how does the second tool know when the first one is finished?

Race condition

As both of them detect file changes they are triggered at the same time, which creates a race condition. The application should be fully started when the browser sends the request, but livereload does not know when that happens.

For example, an Express-based server might work something like this:

const app = express();

// do some initialization

app.get(...);

app.listen(port, () => console.log(`Listening on ${port}`));

Refreshing the browser before the app is initialized results in an error page. So, livereload should wait a little before triggering the refresh.

Running npx livereload . -w 2000 instructs livereload to wait 2 seconds. The app is initialized in this time:

In the video, the app takes ~2 seconds to initialize. But since livereload waits a similar amount, the change in the code is reflected in the browser automatically.

browserapprunningrestartingrunningrestart timenodemonlivereloadwaitingdelay0123restartreloadGETDelayed reload

When the restart time is known and is fairly constant, setting a delay is a good workaround to make sure the browser is not refreshed too soon. But this is not always the case.

Another example is Jekyll which is the engine behind this blog. A file watcher that does an incremental update is built-in, so there is no need to restart it.

When it detects a change, it prints a message that it regenerates the site:

Regenerating: 1 file(s) changed at 2020-07-06 05:55:16
	_drafts/reload_console.md

And when it’s ready, it prints another message:

	...done in 3.635316567 seconds.

It usually takes ~3 seconds, but sometimes it’s a bit longer, depending on what changed. When using livereload, we need to take this into account and use a delay that is greater than most rebuild times.

For example, this uses 5 seconds:

npx livereload . -e 'md' -w 5000

You can see in the video that there is a significant delay between the end of the rebuilding and the reloading.

browserJekyllrunningregeneratingrunningregenerate timelivereloadwaitingdelay0123456reloadGETThe delay is longer than the regenerate time

This is an interesting case of tail latency amplification. If the rebuild usually takes 3 seconds but sometimes it can go up to 5, the delay should be 5 seconds. But this makes every change slower.

Shameless plug
Learn how serverless and Infrastructure-as-Code play together in AWS

Find out more

Event-driven reloading

The problem is that livereload is not driven by the event that the rebuild is complete but that the file is changed and this behavior only gives the delay setting to play with. There is some internal event mechanism in Jekyll and nodemon which in theory can be used to trigger a browser refresh, but that would be a lot of work and that would only work with these two applications.

But there is a universal event bus that can connect the two parts: the console. Jekyll prints an easily-identifiable message when it’s ready, just like the Express app above.

This is the basis of the manual workflow also. Restart the app then wait for the message that it is finished, then reload the page.

Making livereload to trigger when a given text appears on the consloe would make it fully event-driven and minimize the wasted time by waiting too long. And with just a bit of bash magic, it’s possible to come up with a universal solution.

<command> | tee >(grep --line-buffered done > >(npx livereload <(cat) -ee ' '))

When used it with jekyll serve | ..., it results in a timely reload:

You can see that the delay between the end of the rebuilding and the browser refresh is gone and since there is no hardcoded value it adapts to the length of the Jekyll refresh.

Refresh when the trigger word appears on the consolejekylljekyllgrepgreplivereloadlivereloadbrowserbrowserchangeRegenerating: 1 file(s) changed at ..._drafts/reload_console.md...done in 3.1 seconds.reload

The configurable part is the parameter of the grep, in this case it watches for the done text.

The script works by first using tee to clone the output, so it is also printed to the console. Then it uses grep to look for the trigger text (tee >(grep ...)). The --line-buffered is needed to disable buffering as we need every instance of the trigger text as soon as it appears.

The filtered stream is then redirected to a construct that starts livereload (> >(npx livereload ...)). Livereload can only watch files and <(cat) -ee ' ' converts the stdin to a file that it can use.

Shameless plug
Downloading and uploading files in a serverless app in a secure, robust, and scalable way? Yes, it's possible

Learn how

Conclusion

My favorite development tools are little snippets that are not dependent on any particular technology and can be just thrown into the mix to achieve something useful.

Event-driven refreshing of the browser is a great example of such a tool. It does not depend on any event framework and can be easily configured to support all sorts of use-cases.

14 July 2020