I run GrapheneOS on a Pixel as my daily phone, mostly because I got tired of tracking which of my apps quietly talk to Google Play services in the background. Last week a reply from the official GrapheneOS Mastodon account showed up in my feed, answering someone who'd asked when the project would support Motorola hardware. The answer was blunt: initial Motorola devices would be flagship-only, priced above Pixels, and the target was 2027. Lower-end phones would take longer because, in the project's own words, their update and security guarantees "aren't as good."
That timeline is far enough out that most frontend engineers would shrug and move on. I almost did too, until I remembered that GrapheneOS isn't some exotic fork you need a lab device to test against — it ships a real Chromium-based browser called Vanadium, and I already have it installed. So instead of speculating about 2027 hardware, I spent an afternoon actually loading one of our internal PWAs on it and seeing what broke.
Nothing crashed. But two defaults I hadn't accounted for changed how the app behaved badly enough that I'd call them bugs if I'd shipped them to a client without knowing why. This post is about those two defaults, not about Motorola's release calendar.
What GrapheneOS actually said about Motorola
The exchange itself was short. Someone on Mastodon asked whether Motorola phones would ever get official GrapheneOS support, and the GrapheneOS account replied directly instead of pointing to a roadmap doc. That kind of direct answer from a project account carries more weight than a rumor, so it's worth quoting precisely rather than paraphrasing into something vaguer than what was said.
The project confirmed Motorola support is planned, but scoped it tightly: only new flagship models at first, at a higher price point than Pixels, with everything below that tier arriving later once Qualcomm's update commitments extend down the lineup.
"The initial devices with GrapheneOS support should be available in 2027." — GrapheneOS, reply on Mastodon
Why does a hardware timeline matter to me as someone who ships web apps, not firmware? Because up to now, "someone using GrapheneOS" has meant "someone who specifically bought a Pixel to run it," a genuinely narrow and technically motivated audience. Motorola is a mainstream OEM with retail shelf space. Widening the on-ramp doesn't make GrapheneOS mainstream overnight, but it does mean the population of visitors hitting my sites through Vanadium's defaults, rather than stock Chrome, stops being a rounding error I can safely ignore.
Why flagships only, and what Qualcomm has to do with it
The project's explanation for the flagship-only restriction comes down to chipset tiers, not marketing. GrapheneOS requires a verified boot chain, a hardware root of trust, and a firmware update commitment long enough to be worth building an OS around, and according to the project's statement, that combination currently only exists on the latest Snapdragon flagship silicon. Motorola's mid-range and budget phones use Qualcomm chips that don't come with the same multi-year security update guarantee baked in by the vendor.
Getting that changed isn't a software fix on GrapheneOS's side. The project's own framing was that Motorola would need to start paying Qualcomm for longer update support on chips below the flagship tier before those devices could qualify at all. That's a business negotiation between two companies, which is a very different kind of blocker than "the code isn't ready yet."
For anyone thinking about this from a product perspective rather than a purely technical one, the practical read is that the GrapheneOS-on-Motorola audience will look like the GrapheneOS-on-Pixel audience for a long while: people willing to pay a premium for a phone specifically to run a hardened OS on it. That's useful context for prioritization — it tells you this is a "worth a few hours of testing" problem right now, not a "block the release" problem.
The JIT toggle nobody's PWA accounts for
This is the part that actually cost me debugging time. GrapheneOS's own documentation states that Vanadium disables the V8 JIT compiler by default for every site, with a per-site toggle a user has to turn on manually, and the same default applies to apps that embed Vanadium as their WebView. Without the JIT, JavaScript execution falls back to V8's bytecode interpreter, which is correct but nowhere near as fast for anything computationally heavy.
I found this out because an internal dashboard I maintain renders a canvas-based particle visualization on its landing screen, written assuming near-native JS throughput on any reasonably recent phone. Here's roughly what that loop looked like before I touched it:
function renderFrame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const particle of particles) { // 5,000 particles
particle.angle += 0.02;
particle.x += Math.cos(particle.angle) * particle.speed;
particle.y += Math.sin(particle.angle) * particle.speed;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(renderFrame);
}
requestAnimationFrame(renderFrame);
On stock Chrome this holds close to 60fps without complaint. On Vanadium with JIT off, it dropped to somewhere around 8-10fps on my Pixel, which is the kind of stutter that makes a page look broken rather than merely unoptimized. The fix wasn't to chase a "detect GrapheneOS" flag — there isn't a clean one, and I don't want to be in the business of maintaining a browser fingerprint list anyway. Instead I added a quick calibration pass that measures actual throughput on the device and scales the workload to fit a frame budget:
function calibrate(sampleParticles, budgetMs = 8) {
const start = performance.now();
for (let i = 0; i < 500; i++) {
sampleParticles[i % sampleParticles.length].angle += 0.02;
}
const elapsed = performance.now() - start;
const scale = Math.min(1, budgetMs / (elapsed * (particles.length / 500)));
return Math.max(0.1, scale);
}
const perfScale = calibrate(particles);
const activeCount = Math.floor(particles.length * perfScale);
function renderFrame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < activeCount; i++) {
const particle = particles[i];
particle.angle += 0.02;
particle.x += Math.cos(particle.angle) * particle.speed;
particle.y += Math.sin(particle.angle) * particle.speed;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(renderFrame);
}
requestAnimationFrame(renderFrame);
This isn't specific to GrapheneOS at all, which is exactly why I like it better than a browser check. A throttled low-end Android phone benefits from the same calibration pass as a flagship running with its JIT switched off. Measuring real performance and adapting to it is just a better strategy than assuming every device that reports a modern Chromium version number can actually push modern Chromium throughput.
Push notifications without Google's blessing
The other default worth knowing about affects anything that leans on the Push API. On stock Android, web push for a PWA typically rides on Firebase Cloud Messaging through Google Play services running with system-level privilege. GrapheneOS doesn't ship Google Play services at all by default; it offers an optional, unprivileged "sandboxed Google Play" compatibility layer that a user has to install and enable themselves, and it doesn't get the same OS-level access stock Play services does.
That difference is invisible from your app's code. PushManager.subscribe() resolves the same way whether or not there's a working delivery path underneath it, because the browser has no reliable way to tell your JavaScript "by the way, this device has no push transport installed." You get a valid subscription object either way.
subscribe() promise as proof that notifications will arrive, that's the gotcha: it only confirms the browser accepted the subscription request, not that anything on the device can actually deliver the message.The fix I've settled on is cheap enough that I'd recommend it regardless of GrapheneOS: after a user opts into push, send one real test notification immediately and give them an obvious in-app signal ("did that arrive?") instead of silently assuming success. It costs one extra round trip and it catches this failure mode along with a handful of unrelated ones, like notifications silently dying because of battery optimization settings on completely ordinary Android phones.
A short checklist I'm actually running before 2027
None of this warrants a rewrite, and 2027 is genuinely a long way off for a hardware timeline that could still slip. But since I already had Vanadium installed and the perf problem staring at me, I turned the fixes above into a short list I now run whenever I ship anything that touches canvas rendering, WebAssembly, or push:
- Load the feature fresh with Vanadium's per-site JIT toggle left at its default (off), and time it with a stopwatch, not devtools CPU throttling, which doesn't model this accurately.
- Subscribe to push and confirm delivery with an actual test send rather than trusting the subscribe promise.
- Grep for any branch on
navigator.userAgentthat assumes "modern Chromium" implies "fast JIT," since Vanadium's UA string doesn't identify itself as anything unusual. - Toggle GrapheneOS's per-app network permission off for the app and confirm it fails with a visible error instead of hanging silently on a fetch that will never resolve.
- Re-run the above against any Android wrapper app that embeds the same PWA in a WebView, since Vanadium is also GrapheneOS's system WebView and inherits the same defaults there.
Most of these take a few minutes each once the device is in front of you, which is really the whole argument for doing them now instead of waiting for a bug report from a phone you don't own.
What I'm actually doing differently
I haven't rewritten anything around this. What changed is that the calibration pass and the push confirmation step are now in the "default template" I copy for new features, the same way error boundaries and loading states already were. Both are small enough that they'd be defensible even if GrapheneOS never shipped on a single Motorola phone, because they also help ordinary low-end and battery-throttled Android devices that have nothing to do with hardened operating systems.
If your app doesn't do heavy canvas or WASM work and doesn't depend on push, most of this simply doesn't apply to you yet, and that's a fine place to leave it. But a 2027 launch date has a way of turning into "next quarter" faster than anyone plans for, and a two-line calibration check is a lot cheaper to write today than to debug after a support ticket from someone holding an $1,100 Motorola phone you've never held yourself.
Sources: GrapheneOS — reply on Mastodon regarding Motorola support timeline

Comments
No comments yet — be the first to share your thoughts.