flutter_lighthouse 0.1.0 copy "flutter_lighthouse: ^0.1.0" to clipboard
flutter_lighthouse: ^0.1.0 copied to clipboard

Lighthouse for Flutter — drop in, tap once, and it auto-walks every route, scores performance 0–100, and emits actionable findings.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_lighthouse/flutter_lighthouse.dart';

/// Example app for `flutter_lighthouse`.
///
/// Tap the 🏎️ speed button to run an audit. The demo deliberately mixes good
/// and bad: `/feed` (rebuild storm) and `/gallery` (oversized images) score
/// low, while `/optimized` is a properly-built screen that scores 90+ — so the
/// report shows the audit can tell them apart.
void main() {
  runApp(
    LighthouseOverlay(
      config: const LighthouseConfig(
        routes: ['/feed', '/gallery', '/optimized'],
        waitPerRoute: Duration(seconds: 2),
      ),
      child: const BadApp(),
    ),
  );
}

class BadApp extends StatelessWidget {
  const BadApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'flutter_lighthouse example',
      // Audit runs in debug (LighthouseOverlay is a dev tool, and the image
      // collector relies on a debug-only paint hook). Hide the banner so the
      // demo recording is clean.
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0CCE6B)),
        useMaterial3: true,
      ),
      initialRoute: '/',
      routes: {
        '/': (_) => const HomeScreen(),
        '/feed': (_) => const FeedScreen(),
        '/gallery': (_) => const GalleryScreen(),
        '/optimized': (_) => const OptimizedScreen(),
      },
    );
  }
}

/// When true, the home screen kicks off one audit automatically ~2s after
/// launch and prints the report to the console — used to validate findings on
/// a real device without a manual tap. Set via `--dart-define=AUTO_AUDIT=true`.
const _autoAudit = bool.fromEnvironment('AUTO_AUDIT');

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  @override
  void initState() {
    super.initState();
    if (_autoAudit) {
      WidgetsBinding.instance.addPostFrameCallback((_) async {
        await Future<void>.delayed(const Duration(seconds: 2));
        if (!mounted) return;
        final report = await Lighthouse.audit(
          context,
          config: const LighthouseConfig(
            routes: ['/feed', '/gallery', '/optimized'],
            waitPerRoute: Duration(seconds: 3),
          ),
        );
        debugPrint('\n===== LIGHTHOUSE REPORT =====');
        debugPrint('Overall: ${report.overallScore.round()} (${report.grade})');
        for (final m in report.metrics) {
          debugPrint('  ${m.routeName}: frames=${m.frameCount} '
              'p95=${m.p95FrameBuildMs.toStringAsFixed(1)}ms '
              'catastrophic=${m.catastrophicFrames} '
              'rebuilds=${m.rebuildCounts} '
              'imgDecodes=${m.imageDecodeCount} '
              'oversized=${m.oversizedImages.length}');
        }
        debugPrint('--- findings (${report.findings.length}) ---');
        for (final f in report.findings) {
          debugPrint('  [${f.severity.label}] ${f.title}');
        }
        debugPrint('=============================\n');
        if (!mounted) return;
        // Show the visual report (for screenshots / the demo).
        await Navigator.of(context).push(
          MaterialPageRoute<void>(
            builder: (_) => LighthouseReportScreen(report: report),
          ),
        );
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('🛰️ Lighthouse demo')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          const Text(
            'This app is intentionally slow. Tap the speed button to audit it, '
            'or open a screen to feel the jank.',
          ),
          const SizedBox(height: 16),
          _Tile(
            title: '/feed — rebuild storm',
            subtitle: '60 list items rebuild every frame',
            route: '/feed',
          ),
          _Tile(
            title: '/gallery — oversized images',
            subtitle: 'Huge images shown at thumbnail size',
            route: '/gallery',
          ),
          _Tile(
            title: '/optimized — clean screen',
            subtitle: 'const widgets, no jank — should score 90+',
            route: '/optimized',
          ),
        ],
      ),
    );
  }
}

class _Tile extends StatelessWidget {
  const _Tile({required this.title, required this.subtitle, required this.route});

  final String title;
  final String subtitle;
  final String route;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        title: Text(title),
        subtitle: Text(subtitle),
        trailing: const Icon(Icons.chevron_right),
        onTap: () => Navigator.of(context).pushNamed(route),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────
// ANTI-PATTERN 1: rebuild storm.
// A 60fps ticker calls setState on the whole list every frame, and each item
// is NOT const, so all 60 rebuild constantly. LighthouseProbe counts them.
// ─────────────────────────────────────────────────────────────────────────
class FeedScreen extends StatefulWidget {
  const FeedScreen({super.key});

  @override
  State<FeedScreen> createState() => _FeedScreenState();
}

class _FeedScreenState extends State<FeedScreen> {
  Timer? _ticker;
  int _tick = 0;

  @override
  void initState() {
    super.initState();
    // 60Hz setState storm.
    _ticker = Timer.periodic(const Duration(milliseconds: 16), (_) {
      if (mounted) setState(() => _tick++);
    });
  }

  @override
  void dispose() {
    _ticker?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('/feed')),
      body: ListView.builder(
        itemCount: 60,
        itemBuilder: (context, i) => LighthouseProbe(
          label: 'FeedItem',
          // Non-const: rebuilds every tick.
          child: ListTile(
            leading: CircleAvatar(child: Text('$i')),
            title: Text('Post #$i (tick $_tick)'),
            subtitle: const Text('Rebuilding on every frame — bad.'),
          ),
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────
// ANTI-PATTERN 2: oversized images.
// Large source images displayed at thumbnail size with no cacheWidth.
// ─────────────────────────────────────────────────────────────────────────
class GalleryScreen extends StatelessWidget {
  const GalleryScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('/gallery')),
      body: GridView.builder(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
        ),
        itemCount: 30,
        itemBuilder: (context, i) => Padding(
          padding: const EdgeInsets.all(4),
          // 2000×2000 asset rendered at ~120px with no cacheWidth — the decode
          // is ~250× the displayed area. Classic oversized-image waste.
          child: Image.asset('assets/big.jpg', fit: BoxFit.cover),
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────
// THE GOOD SCREEN: built right. const widgets, a lazy builder, no blocking
// work and no rebuild storm — so the audit should score it 90+. This is the
// contrast that proves the tool can tell good from bad.
// ─────────────────────────────────────────────────────────────────────────
class OptimizedScreen extends StatelessWidget {
  const OptimizedScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('/optimized')),
      body: ListView.builder(
        itemCount: 60,
        // const item → no needless rebuilds; no timer, no blocking math.
        itemBuilder: (context, i) => _OptimizedItem(index: i),
      ),
    );
  }
}

class _OptimizedItem extends StatelessWidget {
  const _OptimizedItem({required this.index});

  final int index;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: const Icon(Icons.check_circle, color: Color(0xFF0CCE6B)),
      title: Text('Item #$index'),
      subtitle: const Text('Static, const, lazily built — no jank.'),
    );
  }
}
0
likes
160
points
0
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Lighthouse for Flutter — drop in, tap once, and it auto-walks every route, scores performance 0–100, and emits actionable findings.

Repository (GitHub)
View/report issues

Topics

#performance #audit #devtools #monitoring #lighthouse

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_lighthouse