Elixir Phoenix Optimisations iPhone Safari
If you’re running a Phoenix LiveView application and your iOS Safari users are experiencing frozen pages, slow reconnects, or 10+ second hangs after returning from sleep — this article is for you.
I’ve been running a production Phoenix LiveView application. The app worked great on desktop, but iPhone users kept reporting the same thing: the page would freeze or hang for several seconds after switching back from another app or unlocking their phone. Sometimes the page would never recover at all.
Here’s what was actually happening and how I fixed it.
The Problem
The symptoms were consistent:
- User opens the app on iPhone Safari
- Switches to another app or locks the phone
- Returns to the page — it’s frozen for 5-15 seconds
- Sometimes the page never reconnects and requires a manual refresh
This wasn’t a bug in my code. It was a combination of how iOS Safari handles WebSockets, how Phoenix reconnects after sleep, and some surprising CSS performance issues.
Root Cause Analysis
After digging through Phoenix issues, Elixir forum posts, and a lot of iOS Safari debugging, I found multiple layers to this problem.
1. Phoenix Reconnect Logic Before 1.8.2
When a phone sleeps, iOS Safari kills the WebSocket connection. Phoenix detects the error and immediately starts trying to reconnect — but the page is hidden. The browser throttles or stalls these background reconnection attempts. When the user returns, there’s a stalled reconnect attempt blocking the main thread for seconds.
Phoenix 1.8.2 (PR #6534) fixed this by using the visibilitychange API. It stops reconnect attempts while the page is hidden and immediately reconnects when it becomes visible. This single fix is the biggest improvement.
2. Aggressive LongPoll Fallback
Phoenix LiveView has a longPollFallbackMs option that falls back to HTTP long-polling if the WebSocket handshake takes too long. The commonly recommended value of 2500ms is too aggressive for mobile networks where the initial WebSocket handshake can be slow.
The problem compounds because the longpoll fallback flag is stored in sessionStorage. Once a client falls back, it stays on long-polling until the user opens a new tab.
3. Heavy CSS Blur Effects
This one surprised me. Large CSS blur() values on decorative background elements were causing mobile Safari to stall WebSocket connections for several seconds. The hero section had glow effects with blur-[120px] and blur-[150px] — purely decorative elements that were killing network performance on iOS.
4. Full-Page LiveView for Static Content
The home page was a single LiveView rendering everything — hero section, country grid, testimonials, FAQ, CTA. That meant a full WebSocket connection for a page where 90% of the content was static HTML. Every reconnect issue affected the entire page.
The Fixes
Upgrade Phoenix to 1.8.3
The most impactful change. Update mix.exs:
{:phoenix, "~> 1.8.3"},
Then run:
mix deps.update phoenix
This also pulled in Bandit 1.10.2 and Plug 1.19.1 with their own fixes.
Increase LongPoll Fallback Timeout
In app.js, change the timeout from 2500ms to 5000ms:
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 5000,
params: {_csrf_token: csrfToken},
hooks: hooks,
})
If all your users are on modern browsers, you can also remove longPollFallbackMs entirely to disable the fallback.
Add Visibility Change Safety Net
Even with Phoenix 1.8.3’s built-in fix, I added an extra safety net in app.js that force-reconnects when the page becomes visible:
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
if (liveSocket.socket.connectionState() !== "open") {
liveSocket.socket.disconnect(() => liveSocket.socket.connect())
}
}
})
This covers edge cases where the built-in reconnect logic might not fire quickly enough.
Reduce CSS Blur on Mobile
Replace extreme blur values with responsive alternatives. Mobile gets a lighter blur, desktop keeps the full effect:
<!-- Before -->
<div class="bg-primary/20 blur-[120px] rounded-full" />
<!-- After -->
<div class="bg-primary/20 blur-3xl sm:blur-[120px] rounded-full" />
blur-3xl is 72px — still looks great on mobile but doesn’t stall the browser’s network stack.
Convert Static Pages to Controllers
This was the architectural fix. Instead of rendering the entire home page as a LiveView (with a WebSocket connection), I converted it to a standard Phoenix controller. Only the interactive countries filter section uses a LiveView, embedded via live_render:
# Router - before
live "/", HomeLive, :index
# Router - after
get "/", PageController, :home
The controller template renders all static sections as plain HTML and embeds just the interactive part:
<Layouts.app flash={@flash} current_scope={@current_scope} transparent_header>
<%!-- Static hero, testimonials, FAQ, CTA sections --%>
...
<%!-- Only this section needs a WebSocket --%>
{live_render(@conn, EasyIncWeb.CountriesFilterLive, id: "countries-filter")}
...
</Layouts.app>
The result: 90% of the home page loads instantly as static HTML. No WebSocket means no reconnect problems for that content. Only the countries filter maintains a lightweight LiveView connection.
For JS hooks that were previously managed by LiveView (like a 3D globe animation and scroll-aware header), I added standalone initializers that run on DOMContentLoaded:
document.addEventListener('DOMContentLoaded', () => {
initGlobeStandalone()
initScrollHeaderStandalone()
})
These work alongside the LiveView hooks — the standalone init handles controller pages, the hooks handle LiveView pages.
Other Things Worth Knowing
Bandit vs Cowboy
There are reports of iOS WebSocket issues being resolved by switching from Bandit to Cowboy. If you’re still having problems after the above fixes, it’s worth testing:
# In mix.exs, replace {:bandit, "~> 1.5"} with:
{:plug_cowboy, "~> 2.7"}
# In config.exs:
config :my_app, MyAppWeb.Endpoint,
adapter: Phoenix.Endpoint.Cowboy2Adapter
iCloud Private Relay
Safari has a bug where iCloud Private Relay sends CONNECT requests instead of proper WebSocket upgrade requests. This breaks WebSocket connections on some infrastructure (Cloudflare, GCP load balancers, nginx). There’s no server-side fix — it’s an Apple bug that appears to be resolved in Safari 26.0.1.
TLS Configuration
Safari’s WebSocket implementation is strict with TLS certificate validation and has documented issues when exclusively using TLS 1.3. Make sure your server supports both TLS 1.2 and 1.3.
Summary
The priority order for fixing iOS Safari LiveView performance:
- Upgrade Phoenix to 1.8.3+ — gets you the
visibilitychangereconnect fix - Convert static pages to controllers — eliminates unnecessary WebSocket connections
- Tune
longPollFallbackMs— increase to 5000ms or disable entirely - Audit heavy CSS — large
blur()andbackdrop-filtervalues stall mobile networking - Add
visibilitychangesafety net — belt and suspenders for reconnection - Test Bandit vs Cowboy — if issues persist after everything else
The fundamental lesson: not every page needs a WebSocket. Phoenix makes it easy to use LiveView for everything, but the right architecture is static HTML for static content and LiveView only where you need interactivity. Your iOS users will thank you.