flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
165.75k stars 27.38k forks source link

Web: Cache-able code splitting, Tree Shaking and Routes grouping #50196

Open ansarizafar opened 4 years ago

ansarizafar commented 4 years ago

Currently Flutter for web build command generates a big single Javascript file in release mode.

It is proposed that new routing APIs should allow groping of related routes and then the build command should generate separate Javascript files for each group of routes these files can than be pre-fetched separately. This will help to improve Time to Interactive TTI score and defer or remove unnecessary JavaScript work that occurs during initial page load.

jonahwilliams commented 4 years ago

This is already supported by dart's deferred loading, though there don't seem to be any examples in Flutter. Blast from the past: https://news.dartlang.org/2014/08/dart-16-adds-support-for-deferred.html

For example in flutter you could do:

import 'mywidget.dart' deferred as foo

...

final Future<void> loadedLibrary = foo.loadLibrary();

Widget build(BuildContext context) {
  return FutureBuilder(future: loadedLibrary, (snapshot, context) { return foo.MyWidget(); });
}
ansarizafar commented 4 years ago

@jonahwilliams Thanks for explanation. My proposal is about bundling and fetching a group of related routes at once and after the initial bundle is loaded, pre-fetching other routes bundles in the background. If individual routes/widgets are deferred as you suggested then it will effect app performance.

Flutter framework code should also be shipped and cached in browser as a separate file, this will allow us to only re-fetch app code when it is changed.

jonahwilliams commented 4 years ago

If the goal is reducing downloading, this is better provided by a service worker than by relying on browser caches

ansarizafar commented 4 years ago

The goal is to reduce initial javascript bundle size to improve Time to Interactive TTI score without effecting app performance and updating app by downloading minimum javascript when the new version is available.

Flutter for web can be a game changer for building progressive web apps(PWAs) If we have framework level support the performance enhancing features/techniques that other PWA frameworks are already offering.

jonahwilliams commented 4 years ago

I mean, if you're building a PWA you can just use the service cache. Then the initial bundle size doesn't matter as much because the entire thing gets cached client side. That doesn't save on the initial parsing time, but TTI should be reasonable.

If you're on dev/master, flutter create will include a generated service worker file that knows how to cache both the JavaScript bundle and assets/fonts. If you're using that and TTI isn't good enough, it would be better to file a more specific bug so we can try to make it faster. Route based code splitting is only a single example.

ansarizafar commented 4 years ago

I am on master. A basic flutter for web app with just two routes has a size of 3.6 mb and the size of main.dart.js is 1.3 mb.

It would be a big problem for even an app with few routes on slow mobile networks. I think this proplem can only be solved with proper framework support. I would request Flutter core team to consider this proposal.

ansarizafar commented 4 years ago

I have just tried deferred loading widget of my second route in release mode but it seems that its not working. Dart created two files main.dart.js and main.dart.js_1.part.js.

The file size of main.dart.js is still same 1.3 mb and the size of main.dart.js_1.part.js 66 kb. When I visit second route I can't see main.dart.js_1.part.js dynamically loaded in network tab of devtools which means it is already loaded with main.dart.js. Most of the new javascript frameworks for developing PWAs are of just few KBs is size. If Flutter will ship a huge single javascript file then it would be difficult to use Flutter for web for public facing web apps in production.

yjbanov commented 4 years ago

/cc @kevmoo where should these docs go?

kevmoo commented 4 years ago

@kwalrath – we should have general docs for this somewhere under https://dart.dev/web

kwalrath commented 4 years ago

@kevmoo does this deserve its own page, or could we group it into a page full of tips? If the latter, what other tips belong with this one?

kwalrath commented 4 years ago

Oh actually, we do cover this a bit in https://dart.dev/web/deployment#use-deferred-loading-to-reduce-your-apps-initial-size. Should we make that easier to find from the Flutter web docs?

ditman commented 4 years ago

From what I understand from here, I think the OP is asking to have the flutter framework itself modularized as well, so the initial size of an app that doesn't use some flutter features is reduced.

That sounds like a much bigger project than enabling modularization for flutter app developers.

kevmoo commented 4 years ago

From what I understand from here, I think the OP is asking to have the flutter framework itself modularized as well, so the initial size of an app that doesn't use some flutter features is reduced.

That sounds like a much bigger project than enabling modularization for flutter app developers.

The ROI is also unclear. Deferred loading of a few widgets likely won't improve load time because the deferred loading logic itself has some overhead.

The question: are there big enough chunks of the Flutter framework that are sufficiently disentangled for deferred loading to be worth it.

ditman commented 4 years ago

The ROI is also unclear.

100% agreed. I'm not sure about the entanglement of the framework would make this worth the effort (but initial bundle size sounds like a legitimate concern).

ansarizafar commented 4 years ago

I think its not just a documentation issue. Huge single javascript file is ok for internal web apps on local area network but on web its a real problem and it should be addressed.

initial bundle size sounds like a legitimate concern

Not just initial bundle size but even a single line of code change in app, will trigger download of whole javascript bundle again as framework code is also shipped in he same javascript file. Modern front end web frameworks/bundlers already using tree shaking, code splitting, pre-fetching and grouping of routes into separate bundles to reduce initial bundle size and efficient app updates.

In my opinion this issue would be a biggest hurdle in wide spread adoption of flutter for web.

ditman commented 4 years ago

Not just initial bundle size but even a single line of code change in app, will trigger download of whole javascript bundle again as framework code is also shipped in he same javascript file.

Ah, you're right... as a starter it might be doable to separate the framework code from the app itself, that way if you don't update flutter, your bundle stays cached.

BTW, @ansarizafar isn't this already possible by deferring the load of your App in your main entrypoint? That way your app (and everything else it depends on) should be put on a separate bundle, and then the framework and everything else (except for your main.dart method, which should rarely change) is put in a separate, more cacheable file?

ansarizafar commented 4 years ago

@ditman I think Deferred loading in not working in Flutter as I reported here https://github.com/flutter/flutter/issues/50196#issuecomment-582857895

ditman commented 4 years ago

@ansarizafar it seems you tried to split only one route, not the whole application as I suggested above. In my tests, my 1.4MB app got split in a 900KB, uncompressed, main bundle (the one needed to bootstrap the flutter app) and another 500KB, uncompressed, app bundle (which contained my code and its dependencies).

While experimenting, however, I found a bug feature in the way the main chunk is being emitted that prevents it from being cached effectively. (I filed it to the dart-lang team, you can see it right above this reply, or clicking here.)

ansarizafar commented 4 years ago

@ditman Yes, I was trying to split one route but the size of my initial javascript file main.dart.js remains same 1.3 MB and the a second file main.dart.js_1.part.js 66 kb created but When I visited the second route nothing fetched from the server. The second file should have been fetched separately. Here is the relevant code.

import 'package:tribe/pages/home.dart' deferred as homePage;

class MyApp extends StatelessWidget {
  final Future<void> loadedLibrary = homePage.loadLibrary();
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Tribe App',
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      //  home: LoginPage(),
      initialRoute: '/login',
      routes: {
        '/login': (context) => LoginPage(),
        '/home': (context) => FutureBuilder(
            future: loadedLibrary,
            builder: (snapshot, context) {
              return homePage.HomePage();
            })
      },
    );
  }
}

I think we should ask for help from performance guru @addyosmani I am sure he can give us few tips for improving Flutter for web performance.

ditman commented 4 years ago

Turns out Dart2JS is not designed to reuse chunks across different compilations, and it is a feature that "even a single line of code change in app, will trigger download of whole javascript bundle again", because the compiler will optimize its output depending on how libraries are being used. See the ticket above for a more thorough explanation.

Deferred imports are only used to incrementally download parts of your application as needed, but are not supposed to help with static resource caching.

(I'm not sure if there's any action item left in this ticket.)

ansarizafar commented 4 years ago

I think we should keep this issue open as the problem is not solved and change the issue title to Feature request: Code splitting and tree shaking for Flutter for web .

ditman commented 4 years ago

OK, I'm sorry I couldn't be of more assistance. Let's hope the dart2js compiler adds the facilities needed to make cacheable code splitting a thing.

ferhatb commented 4 years ago

/cc @sigmundch

jhancock4d commented 4 years ago

To me this should copy the same model as Angular with code splitting and it needs to do this transparently so that the same code just works for mobile/desktop and also web and web is intelligent enough to just split automatically. If that means adding code to your routing to define modules, that's ok, but it shouldn't break mobile code.

Right now if you use deferred on an import for a project that is web and android as an example android will fail with "deferred .. library was not loaded". This means that even if that works for web, it doesn't work for mobile or desktop and that's a bug.

This needs to just work and be fully supported or flutter for web can't be used for anything other than LOB apps on web because the initial load time will be FAR too long. It needs to be fully formalized too with a real way of doing this at least automatically on routing so that this is done for free for most people.

shovelmn12 commented 4 years ago

To me this should copy the same model as Angular with code splitting and it needs to do this transparently so that the same code just works for mobile/desktop and also web and web is intelligent enough to just split automatically. If that means adding code to your routing to define modules, that's ok, but it shouldn't break mobile code.

Right now if you use deferred on an import for a project that is web and android as an example android will fail with "deferred .. library was not loaded". This means that even if that works for web, it doesn't work for mobile or desktop and that's a bug.

This needs to just work and be fully supported or flutter for web can't be used for anything other than LOB apps on web because the initial load time will be FAR too long. It needs to be fully formalized too with a real way of doing this at least automatically on routing so that this is done for free for most people.

why not go the way React does it, with a twist. add @Lazy annotation and wrap your lazy loaded widget with some kind of a Suspend widget. it doesn't have to be direct child it could also be a any child down the tree, Suspend only defines a fallback when the widget is lazy loading

@Lazy
class SomeWidget extends StatelessWidget {
}

class MainWidget extends StatelessWidget {
 Widget build(context) => Suspend(
                             onLoading: CircularProgressIndicator(),
                             child: SomeWidget(),
                          );
}

that way for mobile nothing has changed and when compiling for web we know what should be splitted, and here as well as in React the splitted code should include all the imports for SomeWidget.

jhancock4d commented 4 years ago

@shovelmn12 What you're proposing is pretty similar to deferred in dart. The problem is deferred blows up on ios/android etc. and ONLY works on Web.

So if that's the way they want to go, that's fine but it has to not blow up on other platforms.

It would be better to have a more formalized approach however that allows you to export libraries and have the libraries to be lazily loaded with their own navigation as is proposed for navigator2 where every child router is lazy loaded.

shovelmn12 commented 4 years ago

@shovelmn12 What you're proposing is pretty similar to deferred in dart. The problem is deferred blows up on ios/android etc. and ONLY works on Web.

So if that's the way they want to go, that's fine but it has to not blow up on other platforms.

It would be better to have a more formalized approach however that allows you to export libraries and have the libraries to be lazily loaded with their own navigation as is proposed for navigator2 where every child router is lazy loaded.

Yea but as i said @Lazy annotation will only effect web when compiling\building.

markusaksli-nc commented 4 years ago

Removing documentation label since the main point here is the proposal https://github.com/flutter/flutter/issues/50196#issuecomment-584285766.

venkatd commented 3 years ago

For our team, separating framework code from app code from our app would be very helpful.

Our customers are OK with an initial load time, but because we prefer deploying updates several times a week, our users might experience the "first load" many times. It would be preferable to cache the large Flutter framework bundle and only invalidate our app code.

yjbanov commented 3 years ago

@kevmoo Are you actively working on this? If not, let's unassign this issue so it's in the pool of issues for people to start working on.

kevmoo commented 3 years ago

Done!

parammittal16 commented 3 years ago

hi is --fast-start option available for flutter build release command? actually, in release mode, i see a big main.dart.js file but in dev mode, the code is split into many small js files? how can i achieve that in build prod mode? any ideas...?

Thanks in Adv.

sabetAI commented 3 years ago

@parammittal16, I think you have to specify dart2js options for webdev in a build config file, as described in the build_web_compilers page. Though, the documentation is not very straightforward to follow, in my opinion.

Pyrolistical commented 3 years ago

Sounds like we would need a solution that works for both mobile/desktop and web.

Right now on the web I can easily break apart my single page application into a small handful of smaller SPAs, then host it all under the same origin but under separate context paths (/admin/* as an app, /profile/* as an app, everything else under /*). This allows me to fine tune each application and deploy them independently. For an example, see https://microapps.netlify.app

Ideally, I could have the ability in flutter to declare subroutes to output as separate index.html+js files and have it output to a folder per subroute. I can then upload everything to a CDN and it would all work. There would be lengthier loads jumping between those subroutes, but overall the user experiences smaller chunks of load times, improving user experience. This is especially critical for landing pages, which need to be tiny and load under 250ms.

On mobile, all these subroutes would already be available in the downloaded bundle and can even pre-warm during the idle time. This ultimately reduces the initial load time on mobile as well.

aDrIaNo34500 commented 3 years ago

De

bodoczky commented 2 years ago

any news on this? Our company needs it very much

kevmoo commented 2 years ago

@sigmundch – anything we can do here?

sigmundch commented 2 years ago

Unfortunately not much we can do here at the moment, no.

It may be worth splitting this bug into separate pieces. I feel we are discussing several requests together. In particular, let me comment on the top 3 requests I see here:

Make deferred loading work with the flutter framework: One request here is about improving how deferred loading works with the framework (since it appears a lot of code is downloaded in the main unit at the moment). I don't believe this requires anything on the dart2js side, but a deeper study on the framework itself about whether this is working as intended or if the framework needs to be modularized further. We've seen non-flutter apps get as low as 10% of the total code in the initial load when making strategic use of deferred-loading in their code.

Bundling and prefetching: Another discussion above is about bundling and prefetching. This is supported today out of the box: if you know which part files correspond to routes, these part files can be bundled together by concatenating the files (there can be multiple files if routes share some code with one another). If the files are loaded by script tags in the browser, then the application will not download them again, but initialize them when the loadLibrary method is called. @joshualitt has also been experimenting with a flag in dart2js that allows users to predefine how some part files are bundled, in order to reduce the total number of files to download.

Caching and updates: Finally, the third discussion is about caching code even with app updates. There was a related discussion in https://github.com/dart-lang/sdk/issues/40576 that has more details for why this is not feasible today. TL;DR is that nothing in the dart2js stack is designed with in-place updates in mind. Optimizations are global, naming decisions are global too, as a result, small changes in an app can create changes in all artifacts produced by the compiler. Our team has been looking at this problem from a different angle recently: rather than trying to preserve that a deferred output file is stable over application builds, we are studying whether we can allow developers to specify shared components that are compiled once and shared across apps and across builds. If we are successful with this exploration, I can see us having a way of caching the framework code, separate from the app code, and achieve some better results. Another consequence of this effort may be that splitting a single app into multiple apps may allow developers to craft a mechanism to deliver updates more piecemeal (each shared component and subapp now is invalidated separately). I should note that this effort is in early stages and it could easily take multiple quarters to be completed.

I hope the details here brings some clarity about the challenges behind this problem.

Vaibhavsaharan commented 2 years ago

Commenting so that it remains fresh

kevmoo commented 2 years ago

Some more context: we're SUPER focused on getting the canvaskit wasm file size down. That's the biggest win. You'd have to have a MASSIVE Flutter app for code splitting to give any kind of real wins compared to the size of the WASM bundle and the core Dart & Flutter plumbing that ships for all apps.

If you feel your app hits the bar where you're CERTAIN code splitting would help, I'd love more details!

AliEasy commented 1 year ago

Still nothing?!

RajeevRetire100 commented 4 months ago

For my web app, the compressed main.dart.js file size is 2MB, which is quite large. I believe this should be added in the roadmap at least.