How to Split and Parallel‑Load Flutter‑for‑Web JS for Faster Page Loads
This article explains how to reduce the large main.dart.js bundle of Flutter for Web by applying code splitting, deferred component loading, and parallel script injection, showing step‑by‑step implementation, performance results, and future optimization directions.
1. Overview
Flutter for Web (FFW) has been widely adopted since its 2021 release, allowing developers familiar with Flutter to write H5 pages and reuse app code. However, the compiled main.dart.js can be large (e.g., 1.2 MB for a Hello World project), causing slow first‑screen load times.
The main optimization goal is to split the JS bundle and load only the parts needed for the current page.
Page‑load speed can be improved from two angles:
Reduce JS file size
Increase JS loading efficiency
In practice this means:
On‑demand loading : When multiple pages exist, the whole main.dart.js is loaded even if many page‑specific codes are unnecessary. Splitting the code so that only the current page’s JS is loaded reduces bundle size, especially as the number of pages grows.
Parallel loading : After splitting, several JS files of different sizes are generated. Loading them in parallel (when bandwidth permits) saves time compared to sequential loading.
2. Engineering Practice
Similar to Webpack’s code splitting , Flutter provides a deferred component feature. By using the deferred as keyword, libraries can be loaded only when needed.
2.1 Deferred Loading Component
Define a widget in box.dart:
import 'package:flutter/material.dart';
/// A normal widget that will be loaded later
class DeferredBox extends StatelessWidget {
const DeferredBox({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 30,
width: 30,
color: Colors.blue,
);
}
}Use it with deferred import:
import 'box.dart' deferred as box;
class SomeWidget extends StatefulWidget {
const SomeWidget({Key? key}) : super(key: key);
@override
State<SomeWidget> createState() => _SomeWidgetState();
}Load the library in initState and build the widget after the future completes:
class _SomeWidgetState extends State<SomeWidget> {
late Future<void> _libraryFuture;
@override
void initState() {
_libraryFuture = box.loadLibrary();
super.initState();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: _libraryFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text('Error: \\${snapshot.error}');
}
return box.DeferredBox();
}
return const CircularProgressIndicator();
},
);
}
}2.2 Refactoring for Deferred Loading
In the Alibaba seller project, each page’s route was transformed to use deferred loading. The main steps include:
Creating a DeferredLoaderWidget that receives the route settings and triggers the appropriate deferred import.
Mapping route paths to their corresponding loadLibrary functions and widget constructors.
Example of the new route configuration:
class RouteConfiguration {
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
return NoAnimationMaterialPageRoute(
settings: settings,
builder: (context) {
return DeferredLoaderWidget(settings: settings);
},
);
}
}Deferred loader implementation (simplified):
import '../../business/distribution/page/sellerapp_page.dart' deferred as sellerapp;
import '../../business/webmain/page/web_news_detail_page.dart' deferred as web_news_detail;
import '../../debug/page/debug_main_page.dart' deferred as debug;
import '../../ability/common/page/common_page_not_found.dart' deferred as pageNotFound;
import 'package:flutter/material.dart';
typedef WidgetConstructor = Widget Function(Map? params);
var _loadLibraryMap = {
'/sellerapp': sellerapp.loadLibrary,
'/web_news_detail': web_news_detail.loadLibrary,
'/debug': debug.loadLibrary,
};
var _constructorMap = {
'/sellerapp': () => sellerapp.widgetConstructor,
'/web_news_detail': () => web_news_detail.widgetConstructor,
'/debug': () => debug.widgetConstructor,
};During initState the widget parses the route, selects the correct loadLibrary, and stores the future. In build it uses a FutureBuilder to instantiate the page widget once the library is loaded.
2.3 Parallel Loading
After splitting, multiple *.part.js files are generated. By default FFW injects loading code for each part only after the main bundle finishes, which wastes time. To achieve true parallel loading, a patch script is added to index.html that pre‑loads all required parts based on the current route.
<!-- ffw split parallel load, load related part.js according to page path -->
<script id="flutterJsPatchLoad">
var deferredLibraryParts = {};
var deferredPartUris = [];
var base = "";
var hash = window.location.hash.substring(2);
var path = hash.split('?')[0];
if (deferredLibraryParts[path]) {
for (var index in deferredLibraryParts[path]) {
loadScript(deferredPartUris[index]);
}
}
function loadScript(url) {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = base + url;
document.body.appendChild(script);
}
</script>During the build process the script extracts deferredLibraryParts and deferredPartUris from main.dart.js and injects them into the HTML, also replacing the placeholder base with the actual CDN URL.
# Extract parts from main.dart.js and replace placeholders in index.html
parts = reg_find_file_content('./build/web/main.dart.js', r'deferredLibraryParts:{(.*?)}')[0]
uris = reg_find_file_content('./build/web/main.dart.js', r'deferredPartUris:\[(.*?)\]')[0]
str_replace_file_content('./build/web/index.html', r'deferredLibraryParts = {}', f'deferredLibraryParts = {{{parts}}}')
str_replace_file_content('./build/web/index.html', r'deferredPartUris = []', f'deferredPartUris = [{uris}]')After deployment, the patched index.html loads all required parts in parallel, reducing the overall load time to be comparable with the main bundle’s finish time.
3. Effect Analysis
Experiments on the Alibaba seller platform show:
News page load time decreased by 9% after splitting and by an additional 15% after enabling parallel loading.
Download page load time decreased by about 15% after splitting; parallel loading gave little extra benefit because the page is image‑heavy.
4. Future Outlook
Even after splitting, main.dart.js remains around 1.3 MB, leaving room for further engine and code size reductions. Improving the precision of deferred‑component analysis and pre‑loading non‑current page chunks are also planned to accelerate multi‑page navigation.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
