Flutter web applications can sometimes suffer from slow initial load times, which can negatively impact user experience and engagement. This updated article explores various techniques to optimize your Flutter web app's initial load time, based on best practices, official Flutter documentation, and real-world code examples.
1. Customizing Web App Initialization
Flutter 3.22 and later versions provide enhanced APIs for customizing web app initialization. The key to this process is the flutter_bootstrap.js
file.
The flutter_bootstrap.js
File
When you build your Flutter web app, the flutter build web
command generates a flutter_bootstrap.js
file in the build/web
directory. This script contains the necessary JavaScript code to initialize and run your Flutter app.
You can include this script in your index.html
file:
<html>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
Alternatively, you can inline the entire contents using a template token:
<html>
<body>
<script>
{{flutter_bootstrap_js}}
</script>
</body>
</html>
Custom Initialization
To customize the initialization process, you can create your own flutter_bootstrap.js
file in the web
subdirectory of your project. This file can use several special tokens that the build step will substitute:
-
{{flutter_js}}
: Makes theFlutterLoader
object available. -
{{flutter_build_config}}
: Sets metadata produced by the build process. -
{{flutter_service_worker_version}}
: Provides a unique number for the service worker version.
A basic custom flutter_bootstrap.js
might look like this:
{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load();
2. Optimizing Renderer Selection
Flutter web can use either the HTML or CanvasKit renderer. The CanvasKit renderer offers better performance for complex UIs but has a larger initial download size. You can customize the renderer selection based on various factors:
const searchParams = new URLSearchParams(window.location.search);
const renderer = searchParams.get('renderer');
const userConfig = {
renderer: renderer || '',
canvasKitVariant: 'auto',
canvasKitMaximumSurfaces: getCanvasKitMaximumSurfaces(),
};
_flutter.loader.load({
config: userConfig,
});
CanvasKit Surfaces
The canvasKitMaximumSurfaces
option is particularly important for performance. It determines the maximum number of overlay surfaces that the CanvasKit renderer can use.
From the Flutter documentation:
"The maximum number of overlay surfaces that the CanvasKit renderer can use."
The optimal number of surfaces depends on the device's capabilities. Here's an example function to determine this:
function getCanvasKitMaximumSurfaces() {
const memory = navigator.deviceMemory || 4;
const cpuCores = navigator.hardwareConcurrency || 2;
if (memory <= 2 || cpuCores <= 2) {
return 2; // Low-end device
} else if (memory >= 8 && cpuCores >= 6) {
return 8; // High-end device
} else {
return 4; // Medium-range device
}
}
3. Implementing a Landing Page and Lazy Loading
To improve perceived load time and provide a better user experience, implement a landing page that shows while your Flutter app initializes. This approach involves creating a simple HTML/CSS/JavaScript landing page that is displayed immediately, while the Flutter app loads in the background.
HTML Structure
First, update your index.html
file to include the landing page structure:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Your Flutter Web App</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div id="landing-page">
<h1>Welcome to Your App</h1>
<p>Loading awesome content...</p>
<div class="loader"></div>
<button id="start-app-btn" style="display: none;">Start App</button>
</div>
<script src="landing-page.js"></script>
<script>
{{flutter_bootstrap_js}}
</script>
</body>
</html>
CSS Styling
Create a styles.css
file in your web
directory:
body, html {
height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
font-family: Arial, sans-serif;
background-color: #f0f0f0;
}
#landing-page {
text-align: center;
}
.loader {
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#start-app-btn {
padding: 10px 20px;
font-size: 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#start-app-btn:hover {
background-color: #45a049;
}
JavaScript for Landing Page
Create a landing-page.js
file in your web
directory:
let engineInitialized = false;
let appStarted = false;
document.addEventListener('DOMContentLoaded', function() {
const startAppBtn = document.getElementById('start-app-btn');
startAppBtn.addEventListener('click', function() {
if (engineInitialized && !appStarted) {
startApp();
}
});
});
function showStartButton() {
const startAppBtn = document.getElementById('start-app-btn');
startAppBtn.style.display = 'inline-block';
}
function hideLoadingIndicator() {
const loader = document.querySelector('.loader');
if (loader) {
loader.style.display = 'none';
}
}
function hideLandingPage() {
const landingPage = document.getElementById('landing-page');
landingPage.style.display = 'none';
}
window.addEventListener('flutter-initialized', function() {
engineInitialized = true;
hideLoadingIndicator();
showStartButton();
});
function startApp() {
appStarted = true;
hideLandingPage();
// Add any additional logic needed to start your Flutter app
}
Customizing flutter_bootstrap.js
Update your flutter_bootstrap.js
to work with the landing page:
{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load({
onEntrypointLoaded: async function(engineInitializer) {
let appRunner = await engineInitializer.initializeEngine();
await appRunner.runApp();
window.dispatchEvent(new Event('flutter-initialized'));
}
});
This setup creates a simple landing page that displays while your Flutter app is loading. Once the Flutter engine is initialized, the "Start App" button appears, allowing the user to launch the app when ready. This approach improves perceived performance and gives you control over when to transition from the landing page to the Flutter app.
To lazy load your Flutter app, you can further customize the flutter_bootstrap.js
file to delay the initialization until user interaction or other conditions are met.
4. Utilizing the onEntrypointLoaded
Callback
The onEntrypointLoaded
callback allows you to perform custom logic at different stages of the initialization process:
_flutter.loader.load({
onEntrypointLoaded: async function(engineInitializer) {
updateLoadingStatus("Initializing engine...");
const appRunner = await engineInitializer.initializeEngine();
updateLoadingStatus("Running app...");
await appRunner.runApp();
}
});
function updateLoadingStatus(status) {
const loadingElement = document.getElementById('loading-status');
if (loadingElement) {
loadingElement.textContent = status;
}
}
5. Optimizing Asset Delivery and Caching
Ensure that your assets are optimized for web delivery:
- Compress images and use appropriate formats (e.g., WebP).
- Minify JavaScript and CSS files.
- Use gzip or Brotli compression on your server.
Implement effective caching strategies using service workers:
_flutter.loader.load({
serviceWorkerSettings: {
serviceWorkerVersion: {{flutter_service_worker_version}},
},
});
6. Preloading Essential Assets
Preload essential assets to start downloading them as soon as possible:
const preloadAssets = [
'/flutter.js',
'/main.dart.js',
'assets/fonts/Roboto-Regular.ttf'
];
preloadAssets.forEach(asset => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = asset;
link.as = asset.endsWith('.js') ? 'script' :
(asset.endsWith('.ttf') ? 'font' : 'fetch');
link.crossOrigin = 'anonymous';
document.head.appendChild(link);
});
7. Error Handling and Timeouts
Implement error handling and timeouts to prevent indefinite loading states:
const initPromise = _flutter.loader.load(/* config */);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Initialization timed out')), 30000)
);
Promise.race([initPromise, timeoutPromise])
.catch(error => {
console.error('Initialization failed:', error);
showErrorMessage('Initialization failed. Please refresh and try again.');
});
8. Full Example: Modern Counter App
Before we conclude, let's look at a full example of a Flutter web app that implements some of the optimization techniques we've discussed. This example showcases a modern counter app with animations and a gradient background.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Modern Counter',
theme: ThemeData(
primarySwatch: Colors.teal,
brightness: Brightness.dark,
fontFamily: 'Montserrat',
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
int _counter = 0;
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animation = Tween<double>(begin: 1, end: 1.2).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
void _incrementCounter() {
setState(() {
_counter++;
});
_controller.forward().then((_) => _controller.reverse());
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.teal.shade800, Colors.teal.shade200],
),
),
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Modern Counter',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 40),
ScaleTransition(
scale: _animation,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'$_counter',
style: const TextStyle(
fontSize: 72,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: _incrementCounter,
style: ElevatedButton.styleFrom(
foregroundColor: Colors.teal,
backgroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: const Text(
'INCREMENT',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
],
),
),
),
),
);
}
}
This example demonstrates a modern UI with animations, which can benefit from the optimization techniques we've discussed, such as choosing the appropriate renderer and optimizing asset delivery.
For a more in-depth exploration of this project, including the full directory structure and any additional optimizations, you can find the complete repository at: https://github.com/samuelkchris/initial_load
Conclusion
Optimizing Flutter web's initial load time requires a multi-faceted approach. By leveraging Flutter's new initialization APIs, customizing renderer selection, implementing effective loading strategies, and following web performance best practices, you can significantly improve your app's initial load time and overall user experience.
Remember to regularly test your app's performance using tools like Lighthouse or WebPageTest, and stay updated with the latest Flutter web optimizations and best practices.
Top comments (0)