DEV Community

kanta13jp1
kanta13jp1

Posted on

Flutter Web in Production — SEO, Core Web Vitals, and PWA at Scale

Flutter Web in Production — SEO, Core Web Vitals, and PWA at Scale

Flutter Web has evolved well beyond "Flutter running in a browser." It now meets serious web requirements: SEO, Core Web Vitals, and full PWA support. This post covers production-grade techniques from a real live app.

Choosing a Rendering Mode

Flutter Web ships with multiple rendering backends.

# flutter build web --web-renderer canvaskit  (default)
# flutter build web --web-renderer html         (legacy)
# flutter build web --wasm                      (WebAssembly / Dart 3.4+)
Enter fullscreen mode Exit fullscreen mode
Mode Render Quality Initial Load SEO Best For
CanvasKit ★★★ Heavy (~2 MB) Graphics-heavy apps
HTML ★★ Light Content-first sites
Wasm ★★★★ Medium High-performance apps

SEO: Meta Tags and Structured Data

Manage dynamic meta tags in web/index.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- OGP / Twitter Card -->
  <meta property="og:title" content="Jibun K.K. — Life Management">
  <meta property="og:description" content="Replace 21 SaaS tools with one app">
  <meta property="og:image" content="https://example.com/og-image.png">
  <meta name="twitter:card" content="summary_large_image">

  <!-- Structured Data (JSON-LD) -->
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "WebApplication",
    "name": "Jibun K.K.",
    "description": "AI life-management app",
    "applicationCategory": "ProductivityApplication"
  }
  </script>
</head>
Enter fullscreen mode Exit fullscreen mode

Updating from Dart at runtime:

import 'dart:js_interop';

@JS('document.title')
external set documentTitle(String value);

void updatePageTitle(String title) {
  documentTitle = title;
  document.querySelector('meta[property="og:title"]')
      ?.setAttribute('content', title);
}
Enter fullscreen mode Exit fullscreen mode

Core Web Vitals Optimization

LCP — Largest Contentful Paint

class LazyImage extends StatefulWidget {
  const LazyImage({super.key, required this.src, required this.alt});
  final String src;
  final String alt;

  @override
  State<LazyImage> createState() => _LazyImageState();
}

class _LazyImageState extends State<LazyImage> {
  bool _visible = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      setState(() => _visible = true);
    });
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedOpacity(
      opacity: _visible ? 1.0 : 0.0,
      duration: const Duration(milliseconds: 300),
      child: Image.network(
        widget.src,
        semanticLabel: widget.alt,
        cacheWidth: 800,
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

CLS — Cumulative Layout Shift Prevention

class AspectRatioImage extends StatelessWidget {
  const AspectRatioImage({
    super.key,
    required this.src,
    this.aspectRatio = 16 / 9,
  });

  final String src;
  final double aspectRatio;

  @override
  Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: aspectRatio,
      child: Image.network(src, fit: BoxFit.cover),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

INP — Offloading to Isolates

import 'package:flutter/foundation.dart';

Future<List<SearchResult>> searchInBackground(String query) async {
  return compute(_heavySearch, query);
}

List<SearchResult> _heavySearch(String query) {
  return largeDataset
      .where((item) => item.matches(query))
      .toList();
}
Enter fullscreen mode Exit fullscreen mode

PWA Configuration

Optimised manifest.json

{
  "name": "Jibun K.K.",
  "short_name": "Jibun",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#1a1a2e",
  "theme_color": "#f97316",
  "icons": [
    {
      "src": "icons/Icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "icons/Icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "screenshots": [
    {
      "src": "screenshots/desktop.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Service Worker — Offline Support

Extend web/flutter_service_worker.js with a Stale-While-Revalidate strategy for images:

self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(
      caches.open('images-v1').then(async (cache) => {
        const cached = await cache.match(event.request);
        const networkFetch = fetch(event.request).then((response) => {
          cache.put(event.request, response.clone());
          return response;
        });
        return cached || networkFetch;
      })
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

URL Strategy and Deep Links

final _router = GoRouter(
  routes: [
    GoRoute(path: '/', builder: (_, __) => const HomePage()),
    GoRoute(
      path: '/tasks/:taskId',
      builder: (context, state) {
        final taskId = state.pathParameters['taskId']!;
        return TaskDetailPage(taskId: taskId);
      },
    ),
    GoRoute(
      path: '/ai-university/:providerId',
      builder: (context, state) {
        return AiProviderPage(
          providerId: state.pathParameters['providerId']!,
        );
      },
    ),
  ],
);

void main() {
  usePathUrlStrategy(); // removes the # from URLs
  runApp(const MyApp());
}
Enter fullscreen mode Exit fullscreen mode

Firebase Hosting Configuration

{
  "hosting": {
    "public": "build/web",
    "rewrites": [{ "source": "**", "destination": "/index.html" }],
    "headers": [
      {
        "source": "**/*.@(js|css|wasm)",
        "headers": [
          { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
        ]
      },
      {
        "source": "index.html",
        "headers": [
          { "key": "Cache-Control", "value": "no-cache" }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Lighthouse Score Checklist

✅ Performance
  - usePathUrlStrategy()
  - --release build
  - Tree-shake unused packages
  - Convert images to WebP
  - Deferred loading for heavy routes

✅ Accessibility
  - Semantics widgets for ARIA labels
  - FocusTraversalGroup for keyboard navigation
  - Colour contrast ratio ≥ 4.5:1

✅ Best Practices
  - HTTPS enforced
  - CSP headers configured

✅ SEO
  - meta description on every route
  - robots.txt + sitemap.xml
Enter fullscreen mode Exit fullscreen mode

Summary

Running Flutter Web in production requires understanding both the framework and web-platform-specific requirements: SEO, Core Web Vitals, and PWA. Combine the techniques here and you'll routinely hit Lighthouse 90+ on a real production app.


Building a life-management app that replaces 21 SaaS tools with Flutter Web + Firebase Hosting. Follow the journey → @kanta13jp1

Top comments (0)