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?