flutter_perf_lints 0.1.0
flutter_perf_lints: ^0.1.0 copied to clipboard
Analyzer plugin that flags Flutter performance problems: undisposed controllers, full-size image decodes, eager lists, animated Opacity and heavy build work.
Flutter Perf Lints
Lint rules that catch Flutter performance and memory problems while you type: leaked controllers, full-size image decodes, eager lists, animated Opacity, oversized rebuilds and heavy work in build().
Summary.
flutter_perf_lintsis an analyzer plugin built on the official Dart analyzer plugin system (analysis_server_plugin, enabled from the top-levelplugins:section ofanalysis_options.yaml; nocustom_lint, no extra dev dependency, no separate CLI). Its seven rules show up in the IDE and indart analyze: undisposed controllers / focus nodes / subscriptions / timers in aState, images decoded at full resolution,ListView/GridView/Columnbuilt eagerly from a dynamic list,Opacitydriven by an animation,ref.watch(p)where only one property is read,setStatein a State with a hugebuild(), and parsing / regex / sorting / sync I/O insidebuild(). Three rules ship IDE quick fixes. Rules match Flutter types by name, so they keep working on partially resolved code, and every rule is unit-tested withanalyzer_testing.
A lints package that catches Flutter performance and memory problems while you type: a controller
you forgot to dispose, a 4000px image decoded just to be shown at 48px, a list that builds all of
its items at once, an Opacity rebuilt every frame, and a setState that rebuilds a whole screen
because one number changed. The warnings show up in the IDE and in dart analyze, and you don't
add anything to pubspec.yaml.
✨ What it catches #
| Rule | What it catches | Fix | Quick fix |
|---|---|---|---|
undisposed_controller ⚠️ |
AnimationController, TextEditingController, ScrollController, PageController, TabController, FocusNode, ValueNotifier/ChangeNotifier, StreamSubscription, Timer created inside a State that nobody calls dispose()/cancel() on |
Close it in dispose() |
✅ adds the line (and creates dispose() if it doesn't exist) |
image_without_cache_size |
Image.network/asset/file/memory without cacheWidth/cacheHeight, and NetworkImage(...) not wrapped in a ResizeImage |
Decode the image at the size it will be shown at | ✗ |
prefer_list_builder |
ListView(children:)/GridView(children:), or a Column/Row inside a SingleChildScrollView, whose children come from .map(...), List.generate or for |
ListView.builder / GridView.builder |
✅ for the ListView(children: xs.map(...).toList()) shape |
avoid_animated_opacity_widget |
Opacity whose value is animation.value, or inside the builder of an AnimatedBuilder |
FadeTransition |
✗ |
prefer_riverpod_select |
final s = ref.watch(p); and then you only read s.count |
ref.watch(p.select((v) => v.count)) |
✅ |
avoid_setstate_large_subtree |
setState in a State whose build is more than 60 lines or more than 25 widgets |
ValueNotifier + ValueListenableBuilder, or a smaller widget |
✗ |
avoid_heavy_work_in_build |
Inside build: jsonDecode, RegExp(...), DateFormat(...), NumberFormat(...), .sort(), readAsStringSync and friends |
Compute it once (initState, a field, static final, a provider) |
✗ |
⚠️ = warning; the rest are info. All rules are on by default as soon as you enable the package.
📦 Installation #
Requires Dart 3.11+ (Flutter 3.41 or newer). In your project's analysis_options.yaml
(at the root, not in a subfolder), add plugins: as its own top-level section:
plugins:
flutter_perf_lints: ^0.1.0
To pin a specific version:
plugins:
flutter_perf_lints: ^0.1.0
- You don't need to add it to
pubspec.yamlordev_dependencies: the analysis server fetches it by itself. - After any change to
plugins:, restart the analysis server (in VS Code: "Dart: Restart Analysis Server"; in Android Studio: the restart button in the Dart Analysis tab). - The first time takes a few seconds while the plugin is built; after that it is cached.
🔕 Turning a rule off #
A specific rule for the whole project:
plugins:
flutter_perf_lints: ^0.1.0
diagnostics:
avoid_heavy_work_in_build: false
prefer_riverpod_select: false
Or change its severity (error / warning / info):
diagnostics:
undisposed_controller: error
Or for a single line / a single file (note the plugin name before the rule):
// ignore: flutter_perf_lints/image_without_cache_size
Image.asset('assets/logo.png'),
// ignore_for_file: flutter_perf_lints/prefer_list_builder
🧪 Try it quickly #
The example/ folder contains a small app with one deliberate mistake per rule:
cd example
flutter pub get
dart analyze
You should get 12 warnings from the seven rules.
❓ FAQ #
Why does dart analyze show the warnings but flutter analyze doesn't?
Since Flutter 3.47, flutter analyze talks to the analysis server over LSP and exits as soon as
the core analysis finishes, a few seconds before plugins (including this one) send their results.
This comes from the flutter tool, not from the package: the IDE shows the warnings normally, and
dart analyze waits for them. In CI, use dart analyze.
Is this a replacement for custom_lint?
Yes. It is built on Dart's official plugin system (since 3.10), so you don't need custom_lint in
dev_dependencies or a separate command: the same dart analyze and the same IDE.
Do I need Riverpod for prefer_riverpod_select?
No. The rule only looks at the code (ref.watch(...) stored in a variable), so if you don't use
Riverpod it will never fire.
Why does a rule fire on code I'm sure is fine?
The rules are heuristic and match by name, so they can be wrong in cases like:
an Image.asset for an icon that is already small, a DateFormat in the build of a screen that
is rarely built, or a controller closed in a helper that isn't called from dispose(). Use
// ignore: for that case, or turn the rule off.
Can I change the thresholds of avoid_setstate_large_subtree?
Not yet from analysis_options.yaml (the plugin system doesn't provide rule options so far).
The thresholds are 60 lines / 25 widgets; if they don't suit you, turn the rule off or change its
severity.
Do the quick fixes work with dart fix?
No. The plugin system only shows them in the IDE (💡 next to the warning), not in dart fix --apply.
⚖️ Limitations #
undisposed_controllerconsiders a resource closed ifdispose()/cancel()/close()is called on it anywhere in the class, or if it is passed to anything insidedispose()(a loop, a helper). This reduces false alarms but can miss some cases.- A controller that comes from outside (
_c = widget.controller) is not counted, because the State doesn't own it. avoid_heavy_work_in_buildignores callbacks whose names start withon(onTap,onPressed…), but does count builders (itemBuilder,builder) because they run at build time.
🛠 Tech stack #
- Dart analyzer plugin on
analysis_server_pluginandanalyzer(AnalysisRule+SimpleAstVisitor). - IDE quick fixes as
ResolvedCorrectionProducers. - Tests with
analyzer_testing(AnalysisRuleTest) andtest_reflective_loader, against a small stubpackage:flutter, so the suite needs no Flutter SDK.
🏗 Project structure #
| Path | Role |
|---|---|
lib/main.dart |
the plugin entry point the analysis server loads (plugin top-level) |
lib/src/rules/ |
one file per rule |
lib/src/fixes/ |
quick fixes, with their edit planning kept testable on parsed code |
lib/src/utils.dart |
constructor-call matching across resolved / unresolved code, State / build helpers |
test/ |
one test file per rule (positive + negative cases) plus fix tests |
example/ |
a Flutter app with one deliberate violation per rule |
🚀 Development #
git clone https://github.com/youssef336/flutter_perf_lints.git
cd flutter_perf_lints
dart pub get
dart test
dart analyze
cd example && flutter pub get && dart analyze # every rule should fire
📄 License #
👤 Author #
Youssef Belal Zakaria · GitHub @youssef336
⭐ If you like this project, give it a star!