介绍
目前 flutter
对 web
的打包产物优化较少,存在 main.dart.js
单个文件体积过大问题,打包文件名没有 hash
值,如果有使用 CDN
会存在资源不能及时更新问题。本文章会对这些问题进行优化。
优化打包产物体积
从打包产物中可以看到其中 main.dart.js
文件体积较大,且该文件是 flutter web
运行的主要文件之一,该文件体积会随着业务代码的增多而变大,如果不对其体积进行优化,会造成页面白屏时间过长,影响用户体验。

打包产物目录结构:
├── assets // 静态资源文件,主要包括图片、字体、清单文件等│ ├── AssetManifest.json // 资源(图片、视频等)清单文件│ ├── FontManifest.json // 字体清单文件│ ├── NOTICES│ ├── fonts│ │ └─ MaterialIcons-Regular.otf // 字体文件,Material风格的图标│ ├── packages│ │ └─ cupertino_icons // 字体文件│ │ └─ cupertino_icons │ │ └─ assets│ │ └─CupertinoIcons.ttf│ ├── images // 图片文件夹├── canvaskit // canvaskit渲染模式构建产生的文件├── favicon.png├── flutter.js // 主要下载main.dart.js文件、读取service worker缓存等,被index.html调用├── flutter_service_worker.js // service worker的使用,主要实现文件缓存├── icons // pwa应用图标├── index.html // 入口文件├── main.dart.js // JS主体文件,由flutter框架、第三方库、业务代码编译产生的├── manifest.json // pwa应用清单文件└── version.json // 版本文件
对于字体文件,我所使用的 flutter
版本(3.19.0)在 build web
时,默认开启了 tree-shake-icons
,可以自行运行 flutter build web -h
查看。所以优化的重心为 main.dart.js
文件。
打包脚本目录结构:
├── scripts│ ├── buildScript │ │ ├─ build.js // 打包脚本│ │ └─ loadChunk.js // 加载并合并分片脚本
使用 deferred 延迟加载
dart
官方提供了 deferred
关键字来实现 widget
和页面
的延迟加载。
文档
使用 deferred
关键字标识的 widget
或页面
就会从 main.dart.js
文件中抽离出来,生成如 main.dart.js_1.part.js
、main.dart.js_2.part.js
、main.dart.js_x.part.js
等文件,可以一定程度上优化 main.dart.js
文件体积。
参考文章
开启 gzip 压缩
让服务端开启 gzip
压缩
文件分片
可以对 main.dart.js
文件进行分片处理,充分利用浏览器并行加载的机制来节省加载时间。
build.js
中加入分片代码 (文章中代码是在 Flutter web - 2 多项目架构设计 文章基础上修改)
importfs from"fs";importpath from"path";functionsplitFile(){ constchunkCount =5;consttargetFile =path.resolve(buildOutPath,`./main.dart.js`);constfileData =fs.readFileSync(targetFile,"utf8");constfileDataLen =fileData.length;consteachChunkLen =Math.floor(fileDataLen /chunkCount);for(leti =0;i <chunkCount;i++){ conststart =i *eachChunkLen;constend =i ===chunkCount -1?fileDataLen :(i +1)*eachChunkLen;constchunk =fileData.slice(start,end);constchunkFilePath =path.resolve(`./build/${ args.env}/${ args.project}/main.dart_chunk_${ i}.js`);fs.writeFileSync(chunkFilePath,chunk);}fs.unlinkSync(targetFile);}
分片后还需修改 flutter.js
内容,使其加载分片后的文件,在后续步骤中会讲解。
文件名添加 hash 值
在 build.js
中新增:
importfs from"fs";importpath from"path";import{ glob }from"glob";consthashFileMap =newMap();constmainDartJsFileMap ={ };asyncfunctionhashFile(){ constfiles =awaitglob(["**/main.dart_chunk_@(*).js"],{ cwd:buildOutPath,nodir:true,});for(leti =0;i <files.length;i++){ constoldFilePath =path.resolve(buildOutPath,files[i]);constnewFilePath =oldFilePath.substring(0,oldFilePath.length -path.extname(oldFilePath).length )+"."+getFileMD5({ filePath:oldFilePath })+path.extname(oldFilePath);fs.renameSync(oldFilePath,newFilePath);constoldFileName =path.basename(oldFilePath);constnewFileName =path.basename(newFilePath);hashFileMap.set(oldFileName,{ oldFilePath,newFilePath,newFileName,});if(oldFileName.includes("main.dart_chunk"))mainDartJsFileMap[oldFileName]=newFileName;}}functiongetFileMD5(options){ const{ fileContent,filePath }=options;const_fileContent =fileContent ||fs.readFileSync(filePath);consthash =crypto.createHash("md5");hash.update(_fileContent);returnhash.digest("hex").substring(0,8);}
修改 flutter.js
内容
查看 flutter.js
文件代码可以发现,main.dart.js
是由 flutter.js
中 loadEntrypoint
函数加载的,实际是通过调用 _createScriptTag
函数,在 DOM
中插入了有 main.dart.js
地址的 script
标签。
asyncloadEntrypoint(e){ let{ entrypointUrl:r =`${ l}main.dart.js`,onEntrypointLoaded:t,nonce:i,}=e ||{ };returnthis._loadEntrypoint(r,t,i);}_loadEntrypoint(e,r,t){ leti =typeofr =="function";if(!this._scriptLoaded){ this._scriptLoaded =!0;leto =this._createScriptTag(e,t);if(i)console.debug("Injecting `;consthtmlPath =path.resolve(buildOutPath,"./index.html");lethtmlText =fs.readFileSync(htmlPath).toString();constheadEndIndex =htmlText.indexOf("");htmlText =htmlText.substring(0,headEndIndex)+bridgeScript +htmlText.substring(headEndIndex);fs.writeFileSync(htmlPath,Buffer.from(htmlText));}
完整代码
需安装依赖:pnpm i chalk crypto terser glob @babel/core commander @babel/preset-env -D
importfs from"fs";importpath from"path";import{ glob }from"glob";importcrypto from"crypto";import{ minify_sync }from"terser";import{ exec }from"child_process";import{ transform }from"@babel/core";import{ program,Option }from"commander";program .command("build").requiredOption("-p, --project ","project name").addOption(newOption("-e, --env ","dev or prod environment").choices(["dev","prod"]).default("dev")).addOption(newOption("--web-renderer ","web renderer mode").choices(["auto","html","canvaskit"]).default("auto")).action((cmd)=>{ build(cmd);});program.parse(process.argv);functionbuild(args){ constbuildTargetPath =path.resolve(`./lib/${ args.project}`);constbuildOutPath =path.resolve(`./build/${ args.env}/${ args.project}`);constbaseHref =`/${ args.project}/`;consthashFileMap =newMap();constmainDartJsFileMap ={ };fs.rmSync(buildOutPath,{ recursive:true,force:true});constcommandStr =`fvm flutter build web --base-href ${ baseHref}--web-renderer ${ args.webRenderer}--release --pwa-strategy none -o ${ buildOutPath}--dart-define=INIT_ENV=${ args.env}`;exec(commandStr,{ cwd:buildTargetPath,},async(error,stdout,stderr)=>{ if(error){ console.error(`exec error: ${ error}`);return;}console.log(`stdout: ${ stdout}`);splitFile();awaithashFile();insertLoadChunkScript();if(stderr){ console.error(`stderr: ${ stderr}`);return;}});functionsplitFile(){ constchunkCount =5;consttargetFile =path.resolve(buildOutPath,`./main.dart.js`);constfileData =fs.readFileSync(targetFile,"utf8");constfileDataLen =fileData.length;consteachChunkLen =Math.floor(fileDataLen /chunkCount);for(leti =0;i <chunkCount;i++){ conststart =i *eachChunkLen;constend =i ===chunkCount -1?fileDataLen :(i +1)*eachChunkLen;constchunk =fileData.slice(start,end);constchunkFilePath =path.resolve(`./build/${ args.env}/${ args.project}/main.dart_chunk_${ i}.js`);fs.writeFileSync(chunkFilePath,chunk);}fs.unlinkSync(targetFile);}asyncfunctionhashFile(){ constfiles =awaitglob(["**/main.dart@(*).js"],{ cwd:buildOutPath,nodir:true,});for(leti =0;i <files.length;i++){ constoldFilePath =path.resolve(buildOutPath,files[i]);constnewFilePath =oldFilePath.substring(0,oldFilePath.length -path.extname(oldFilePath).length )+"."+getFileMD5({ filePath:oldFilePath })+path.extname(oldFilePath);fs.renameSync(oldFilePath,newFilePath);constoldFileName =path.basename(oldFilePath);constnewFileName =path.basename(newFilePath);hashFileMap.set(oldFileName,{ oldFilePath,newFilePath,newFileName,});if(oldFileName.includes("main.dart_chunk"))mainDartJsFileMap[oldFileName]=newFileName;}}functiongetFileMD5(options){ const{ fileContent,filePath }=options;const_fileContent =fileContent ||fs.readFileSync(filePath);consthash =crypto.createHash("md5");hash.update(_fileContent);returnhash.digest("hex").substring(0,8);}functioninsertLoadChunkScript(){ letloadChunkContent =fs .readFileSync(path.resolve("./scripts/buildScript/loadChunk.js")).toString();loadChunkContent =loadChunkContent .replace('const mainDartJsFileMapJSON = "{ }";',`const mainDartJsFileMapJSON = '${ JSON.stringify(mainDartJsFileMap)}';`).replace("${ baseHref}",`${ baseHref}`);constparseRes =transform(loadChunkContent,{ presets:["@babel/preset-env"],});constterserRes =minify_sync(parseRes.code,{ compress:true,mangle:true,output:{ beautify:false,comments:false,},});if(!fs.existsSync(path.resolve(buildOutPath,"script")))fs.mkdirSync(path.resolve(buildOutPath,"script"));constloadChunkJsHash =getFileMD5({ fileContent:terserRes.code });fs.writeFileSync(path.resolve(buildOutPath,`./script/loadChunk.${ loadChunkJsHash}.js`),Buffer.from(terserRes.code));constpattern =/_createScriptTag\([\w,]+\){ (.*?)}/;constflutterJsPath =path.resolve(buildOutPath,"./flutter.js");letflutterJsContent =fs.readFileSync(flutterJsPath).toString();flutterJsContent =flutterJsContent.replace(pattern,(match,p1)=>{ return`_createScriptTag(){ let t=document.createElement("script");t.type="application/javascript";t.src='${ baseHref}script/loadChunk.${ loadChunkJsHash}.js';return t}`;});fs.writeFileSync(flutterJsPath,Buffer.from(flutterJsContent));constflutterJsHashName =`flutter.${ getFileMD5({ fileContent:flutterJsContent,})}.js`;fs.renameSync(flutterJsPath,path.resolve(buildOutPath,flutterJsHashName));constbridgeScript =``;consthtmlPath =path.resolve(buildOutPath,"./index.html");lethtmlText =fs.readFileSync(htmlPath).toString();constheadEndIndex =htmlText.indexOf("");htmlText =htmlText.substring(0,headEndIndex)+bridgeScript +htmlText.substring(headEndIndex);fs.writeFileSync(htmlPath,Buffer.from(htmlText));}}
存在问题
目前只处理的 main.dart_chunk_i.js
等分片文件,未对延迟加载文件、图片、字体等文件进行处理。
参考文章
Flutter Web 在《一起漫部》的性能优化探索与实践
Flutter for Web 首次首屏优化——JS 分片优化