Vicen Moreno

Pro Googler

Follow me on GitHub

Flutter Web en producción - Lecciones aprendidas

Flutter para web suena genial en teoría. ¿Cómo funciona en la práctica?

El experimento

Dashboard interno de empresa. Requisitos:

  • Compartir código con app móvil existente (Flutter)
  • Usuarios internos (~200)
  • Gráficos, tablas, formularios

Decidimos usar Flutter Web. Aquí está nuestra experiencia después de 8 meses.

Setup inicial

flutter create --platforms=web,ios,android my_dashboard
cd my_dashboard
flutter run -d chrome

Fácil. Pero los problemas empiezan después.

Renderers: HTML vs CanvasKit

Flutter Web tiene dos renderers:

HTML renderer

flutter build web --web-renderer html
  • Pros: Bundle más pequeño, mejor SEO
  • Contras: Rendering inconsistente, menos features

CanvasKit renderer

flutter build web --web-renderer canvaskit
  • Pros: Pixel-perfect con móvil, todas las features
  • Contras: Bundle de 2MB+, carga inicial lenta

Auto (default)

flutter build web --web-renderer auto

Usa HTML en móviles, CanvasKit en desktop.

Nuestra elección: CanvasKit (dashboard interno, usuarios en desktop).

Problema 1: Carga inicial lenta

Síntoma

First Contentful Paint: 4.5s
Time to Interactive: 6.2s

Inaceptable.

Solución: Deferred loading

// Lazy load de features pesadas
import 'package:my_app/heavy_feature.dart' deferred as heavy;

Future<void> loadHeavyFeature() async {
  await heavy.loadLibrary();
  heavy.HeavyWidget();
}

Solución: Loading indicator mientras carga

<!-- web/index.html -->
<body>
  <!-- Loading spinner mientras Flutter carga -->
  <div id="loading">
    <style>
      #loading {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
      }
      .spinner {
        width: 50px;
        height: 50px;
        border: 3px solid #f3f3f3;
        border-top: 3px solid #3498db;
        border-radius: 50%;
        animation: spin 1s linear infinite;
      }
      @keyframes spin {
        0% { transform: rotate(0deg); }
        100% { transform: rotate(360deg); }
      }
    </style>
    <div class="spinner"></div>
  </div>

  <script>
    window.addEventListener('flutter-first-frame', function() {
      document.getElementById('loading').remove();
    });
  </script>

  <script src="flutter.js" defer></script>
</body>

Resultado: Perceived load time de 4.5s → 1.5s (spinner aparece rápido).

Problema 2: SEO inexistente

Flutter Web renderiza en canvas. Google no puede leer el contenido.

Para dashboards internos

No importa. No necesitas SEO.

Para sitios públicos

Flutter Web no es recomendado para contenido que necesita SEO.

Alternativas:

  • Landing page en HTML/Next.js
  • App Flutter embebida para features interactivas

Problema 3: Text selection rota

Síntoma

Usuarios no podían copiar texto.

Solución

SelectableText('Este texto se puede copiar');

// O envolver en SelectionArea
SelectionArea(
  child: Column(
    children: [
      Text('Este también'),
      Text('Y este'),
    ],
  ),
)

Problema 4: Scroll no nativo

Síntoma

Scroll se sentía raro, especialmente en trackpads de Mac.

Solución

// main.dart
void main() {
  // Usar scroll behavior de la plataforma
  runApp(
    MaterialApp(
      scrollBehavior: const MaterialScrollBehavior().copyWith(
        dragDevices: {
          PointerDeviceKind.mouse,
          PointerDeviceKind.touch,
          PointerDeviceKind.trackpad,
        },
      ),
      home: MyApp(),
    ),
  );
}

Problema 5: URLs y navegación

Síntoma

URLs no cambiaban al navegar. Back button no funcionaba.

Solución: go_router

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => DashboardScreen(),
    ),
    GoRoute(
      path: '/users/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return UserDetailScreen(userId: id);
      },
    ),
  ],
);

// main.dart
MaterialApp.router(
  routerConfig: router,
)

Ahora:

  • URLs reflejan navegación
  • Back button funciona
  • Deep links funcionan

Problema 6: Keyboard shortcuts

Síntoma

Ctrl+C, Ctrl+V no funcionaban como esperado.

Solución

// Capturar shortcuts globales
Shortcuts(
  shortcuts: {
    LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC):
        CopyIntent(),
    LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV):
        PasteIntent(),
  },
  child: Actions(
    actions: {
      CopyIntent: CallbackAction<CopyIntent>(
        onInvoke: (intent) => handleCopy(),
      ),
      PasteIntent: CallbackAction<PasteIntent>(
        onInvoke: (intent) => handlePaste(),
      ),
    },
    child: MyApp(),
  ),
)

Problema 7: Responsive design

Flutter no tiene CSS media queries. Hay que hacerlo manual.

class ResponsiveLayout extends StatelessWidget {
  final Widget mobile;
  final Widget tablet;
  final Widget desktop;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth < 600) {
          return mobile;
        } else if (constraints.maxWidth < 1200) {
          return tablet;
        } else {
          return desktop;
        }
      },
    );
  }
}

// Uso
ResponsiveLayout(
  mobile: MobileNavigation(),
  tablet: TabletNavigation(),
  desktop: DesktopSidebar(),
)

Build y deployment

Optimización de build

flutter build web \
  --web-renderer canvaskit \
  --release \
  --tree-shake-icons \
  --pwa-strategy=none

Hosting en Firebase

firebase init hosting
# public directory: build/web
# single-page app: yes

firebase deploy

Caching headers

// firebase.json
{
  "hosting": {
    "headers": [
      {
        "source": "**/*.@(js|css|wasm)",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "max-age=31536000"
          }
        ]
      },
      {
        "source": "**/*.@(jpg|jpeg|png|gif|webp)",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "max-age=31536000"
          }
        ]
      }
    ]
  }
}

Métricas finales

Métrica Inicial Optimizado
Bundle size 4.2MB 2.8MB
FCP 4.5s 1.5s
TTI 6.2s 3.1s
Lighthouse 45 72

No perfecto, pero aceptable para dashboard interno.

¿Cuándo usar Flutter Web?

Usar cuando:

  • Dashboard/admin interno
  • Aplicación altamente interactiva (tipo Figma)
  • Ya tienes app Flutter móvil y quieres compartir código
  • SEO no importa
  • Usuarios en desktop con buena conexión

NO usar cuando:

  • Sitio público que necesita SEO
  • Content-heavy website
  • Usuarios principalmente en móvil
  • Performance crítica (e-commerce checkout)
  • No tienes app Flutter existente

Alternativas

Para web pública:

  • Next.js: React + SSR + SEO
  • Nuxt: Vue + SSR + SEO
  • Astro: Static + Islands

Para dashboards:

  • Flutter Web (si ya usas Flutter)
  • React + Ant Design
  • Vue + Vuetify

Conclusión

Flutter Web funciona para casos específicos. No es reemplazo de desarrollo web tradicional.

Para nosotros, valió la pena: 70% de código compartido con app móvil, un solo equipo manteniendo todo.

¿Has usado Flutter Web en producción? ¿Qué problemas encontraste?


 Anterior      Posterior

Por Vicente José Moreno Escobar el 25 de octubre de 2024
Archivado en: Flutter   Web   Performance



Puedes disfrutar de otros artículos como éste en el archivo del sitio.