One of the biggest shifts developers face when building real-world apps with Phoenix LiveView is how to handle large, changing datasets. In traditional server-rendered apps, you load a page, dump out a list of data, and that’s that. But LiveView introduces new challenges - and opportunities - when you need to display hundreds or thousands of dynamic records, especially when they’re frequently updated, inserted, or deleted. The old way doesn’t scale.
Enter Phoenix.LiveView.stream/3
. This function is one of the most important - yet underutilized - tools in the LiveView toolkit. It’s designed to let you efficiently manage and render collections of data that change over time, without re-rendering the entire list or chewing up CPU. It’s the missing piece for building fast, reactive interfaces at scale.
To understand why streams matter, you first have to understand how LiveView rendering works. When you update an assign in a LiveView, LiveView calculates the diff between the old and new state, sends that diff to the client over the socket, and the client applies it to the DOM. This is usually fast and efficient - unless you’re dealing with large collections.
If your assign contains, say, a list of 500 items and you replace that entire list in one update, LiveView will re-render the entire list on the server, compute the diff, and ship it to the client. That works - but it’s wasteful. Most of the items haven’t changed. And if you do this repeatedly - such as polling or responding to PubSub events - you quickly end up with unnecessary renders and network usage.
That’s where streams shine. Instead of reassigning a whole list, streams let you surgically insert, delete, or update individual items within a collection. You define the stream with stream(socket, :items, initial_items)
, and then later use functions like stream_insert
, stream_delete
, or stream_update
to modify it. LiveView takes care of the rest - rendering only the changed item and diffing only what’s necessary.
The difference in performance is enormous. With a stream, you can push updates to large tables or feeds with minimal CPU and virtually no perceived latency. And because streams are keyed - meaning each item must have a unique ID - LiveView can efficiently track and replace only the DOM elements that changed.
Streams are also ideal for real-time apps. Imagine a dashboard where new events come in every few seconds, or a support ticket queue where multiple agents are updating the same list. With streams, you can subscribe to updates via PubSub, and then use stream_insert/3
to push the new event to the top of the list. No full re-render, no flicker, just fast, targeted updates.
One common pattern is using stream_insert/4
with at: 0
to prepend items, or appending them at the bottom for infinite scrolling. You can also cap the stream length - removing old items as new ones come in - to avoid memory bloat. LiveView doesn’t do this automatically, but you can easily manage it yourself in your handle_info
or handle_event
callbacks.
Another benefit of streams is that they play well with Live Components. If your list items are dynamic or interactive - like rows with buttons, modals, dropdowns - wrapping each one in a stateful live_component
can help isolate their logic and reduce parent LiveView complexity. Streams integrate seamlessly with this pattern. When you update or insert a component-backed item, LiveView knows how to keep that component’s state intact and re-render only the right piece of the DOM.
You don’t need to use streams everywhere. For static lists that don’t change often, a traditional assign is fine. But for anything interactive, real-time, or large in volume, streams should be your go-to. They’re not just an optimization - they’re a foundational tool for writing scalable LiveView apps.
Another edge case worth noting: streams are socket-specific. Each user’s stream is isolated, so you can safely apply user-specific filtering or visibility logic without worrying about affecting other sessions. This is especially useful for personalized feeds, multi-user queues, or dashboards scoped to a team or role.
You can also pair streams with pagination and infinite scrolling. Instead of preloading hundreds of items, load a page of results, stream them in, and then append more on demand as the user scrolls. This hybrid approach gives you both initial performance and dynamic updates. And because streams manage their internal state, you don’t have to juggle list mutations manually.
Debugging stream behavior can take some getting used to. Unlike assigns, streams don’t show up directly in assigns - they’re stored in an internal structure. But you can still inspect their state with socket.streams[:key]
, which gives you access to the order and keys of items in the stream. Logging this during development is a great way to understand how your list evolves over time.
Ultimately, streams reflect the broader philosophy of LiveView: efficient reactivity without complexity. You don’t need a state management library. You don’t need client-side diffing. You just tell LiveView what changed, and it updates the DOM intelligently. Streams extend that model to large, dynamic data - giving you the power to build interfaces that feel fast, even under heavy load.
If you’ve ever struggled with sluggish LiveView performance in big lists, or found yourself re-rendering entire tables on every change, streams are the solution you’ve been looking for. They unlock a level of control and performance that makes LiveView not just viable, but delightful, at scale.
If you're building dynamic interfaces with Phoenix LiveView - tables, dashboards, feeds, notifications, or anything with frequent updates - I’ve created a detailed PDF guide: Phoenix LiveView: The Pro’s Guide to Scalable Interfaces and UI Patterns. It’s a 20-page deep dive into advanced features like streams, real-time patterns, component composition, and architectural best practices. Whether you're scaling an internal tool or launching a polished production UI, this guide will help you build fast, elegant, and maintainable LiveView applications.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.