We’re Fly.io. We run apps for our users on hardware we host around the world. This post is about Elixir Processes and how they work as Phoenix LiveViews. Fly.io happens to be a great place to run Phoenix applications. Check out how to get started!
LiveView lets users get up to speed quickly by being easy to learn, especially with the familiar mount, render and event handler system that React developers will immediately recognize. But LiveView differs from other front-end frameworks in one very consequential way:
A LiveView is a process.
And even though the LiveView docs immediately call out that fact:
A LiveView is a process that receives events, updates its state, and render updates to a page as diffs.
This fact sets LiveView apart from almost every other front-end framework and is so important that I think it could use a little reiteration.
In Elixir and Erlang, ‘processes’ do not refer to operating system processes or threads. They are what’s sometimes called green threads or actors. On a single core CPU they run concurrently and are scheduled and managed by the Erlang Virtual Machine; on multiple cores they run in parallel. Each process in Elixir and Erlang needs ~300 words of memory, and takes microseconds to start, so they are incredibly cheap. In the Erlang Virtual Machine, everything that executes code is running in a process.
I highly recommend you check out this excellent guide from the Elixir website that goes into some of the details about processes. We’re going to keep it slightly higher level here.
Each Process can execute code, it has a first-in-first-out mailbox that any other process can send messages to, and it can also send messages. Each process is sequential, meaning it can only handle one message at a time. The Erlang VM operates kind of like an Operating System scheduler, where it can start and pause or “preempt” work whenever it wants. While it is waiting for a message, your process is completely ignored by the scheduler and doesn’t burn up precious resources.
When we work with GenServers, which are higher level abstractions on top of processes, our flow looks kind of like this:
A process starts and sets its initial state, then it waits for a message. When it receives a message it handles it, gets a new state and returns to waiting. This is an important thing to understand: a process is only able to execute if it receives a message. When it is started or initialized a process executes some code, then it waits for a message. This means an idle process won’t consume resources. There are also some messages that are handled internally by GenServers for you, but that’s outside the scope for this post.
Since an individual process is sequential, if the message handling function takes a long time to execute, the mailbox or queue may back up. If the process is not expecting a ton of new messages this can be okay. While using a Task it is okay to do a costly calculation because those processes don’t expect more messages. While in the case of a LiveView process, a user will see a page that’s unable to respond to events or render updates, this is bad.
Just like any other process, LiveView follows a specific lifecycle. Here’s a simplified flow chart to illustrate this:
Where assigns is our state and event is a special case of a message we make special callbacks for. Every user event or params event is a message being handled by our callbacks. So let’s think through some of the implications of this.
Despite each user having their own process, it’s not an issue due to the lightweight nature of these processes. The benefits we gain in terms of performance and scalability make it well worth it. To be clear: if you do a normal HTTP request using Phoenix controllers that connection gets its own Process too, it just is immediately killed once you’ve sent the response and closed it. In LiveView we keep that process alive.
Every message is handled sequentially, meaning we need to make sure that handle_event, mount, handle_params, and handle_info functions return quickly since they could be blocking a user interaction.
If you have some slow job, query or calculation you should use the built-in async primitives, such as Tasks. Berenice wrote an excellent post showing an example of doing that just in her recent Async Processing in LiveView post.
Assigns are kept for the entire lifetime of the page for every user, and you can quickly chew up memory by shoving a ton of stuff into it. It is okay to re-query stuff you need, and look into using streams when you have a long list of items.
In conclusion, the process is the heart of the Erlang VM, making programming in Elixir a uniquely fast, resilient, and special experience. On purpose, this is only scratching the surface of what a Process is because if the Phoenix team is doing their job right, you shouldn’t need to know much more to be productive and effective at building scalable applications.
When you are ready to learn more, here are two great resources: