Speed Phase 1+2: Deferred loading for 8 heavy screens (code-split for web), branded pre-Flutter splash screen with spinner, preconnect hints, remove dead imports

This commit is contained in:
Patrick Britton 2026-02-10 18:38:40 -06:00
parent 6c0de3b439
commit 96468521cf
3 changed files with 133 additions and 39 deletions

BIN
media/facebook/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View file

@ -4,24 +4,28 @@ import 'package:go_router/go_router.dart';
import 'package:latlong2/latlong.dart';
import '../services/auth_service.dart';
import '../services/api_service.dart';
// Eager (critical path)
import '../screens/home/feed_personal_screen.dart';
import '../screens/home/home_shell.dart';
import '../screens/auth/auth_gate.dart';
// Eager (admin small, behind role check)
import '../screens/admin/admin_dashboard_screen.dart';
import '../screens/admin/admin_scaffold.dart';
import '../screens/admin/admin_user_base_screen.dart';
import '../screens/admin/moderation_queue_screen.dart';
import '../screens/admin/admin_content_tools_screen.dart';
import '../screens/beacon/beacon_screen.dart';
import '../screens/home/feed_personal_screen.dart';
import '../screens/home/home_shell.dart';
import '../screens/quips/create/quip_creation_flow.dart';
import '../screens/quips/feed/quips_feed_screen.dart';
import '../screens/profile/viewable_profile_screen.dart';
import '../screens/profile/blocked_users_screen.dart';
import '../screens/auth/auth_gate.dart';
import '../screens/discover/discover_screen.dart';
import '../screens/secure_chat/secure_chat_full_screen.dart';
import '../screens/secure_chat/secure_chat_loader_screen.dart';
import '../screens/post/threaded_conversation_screen.dart';
import '../screens/notifications/notifications_screen.dart';
// Deferred (code-split for web)
import '../screens/beacon/beacon_screen.dart' deferred as beacon_lib;
import '../screens/quips/create/quip_creation_flow.dart' deferred as quip_create_lib;
import '../screens/quips/feed/quips_feed_screen.dart' deferred as quips_feed_lib;
import '../screens/profile/viewable_profile_screen.dart' deferred as profile_lib;
import '../screens/profile/blocked_users_screen.dart' deferred as blocked_lib;
import '../screens/secure_chat/secure_chat_full_screen.dart' deferred as secure_chat_lib;
import '../screens/secure_chat/secure_chat_loader_screen.dart' deferred as chat_loader_lib;
import '../screens/post/threaded_conversation_screen.dart' deferred as threaded_lib;
/// App routing config (GoRouter).
class AppRoutes {
@ -54,25 +58,37 @@ class AppRoutes {
GoRoute(
path: '$userPrefix/:username',
parentNavigatorKey: rootNavigatorKey,
builder: (_, state) => UnifiedProfileScreen(
handle: state.pathParameters['username'] ?? '',
builder: (_, state) => _deferred(
profile_lib.loadLibrary,
() => profile_lib.UnifiedProfileScreen(
handle: state.pathParameters['username'] ?? '',
),
),
),
GoRoute(
path: quipCreate,
parentNavigatorKey: rootNavigatorKey,
builder: (_, __) => const QuipCreationFlow(),
builder: (_, __) => _deferred(
quip_create_lib.loadLibrary,
() => quip_create_lib.QuipCreationFlow(),
),
),
GoRoute(
path: secureChat,
parentNavigatorKey: rootNavigatorKey,
builder: (_, __) => const SecureChatFullScreen(),
builder: (_, __) => _deferred(
secure_chat_lib.loadLibrary,
() => secure_chat_lib.SecureChatFullScreen(),
),
routes: [
GoRoute(
path: ':id',
parentNavigatorKey: rootNavigatorKey,
builder: (_, state) => SecureChatLoaderScreen(
conversationId: state.pathParameters['id'] ?? '',
builder: (_, state) => _deferred(
chat_loader_lib.loadLibrary,
() => chat_loader_lib.SecureChatLoaderScreen(
conversationId: state.pathParameters['id'] ?? '',
),
),
),
],
@ -80,8 +96,11 @@ class AppRoutes {
GoRoute(
path: '$postPrefix/:id',
parentNavigatorKey: rootNavigatorKey,
builder: (_, state) => ThreadedConversationScreen(
rootPostId: state.pathParameters['id'] ?? '',
builder: (_, state) => _deferred(
threaded_lib.loadLibrary,
() => threaded_lib.ThreadedConversationScreen(
rootPostId: state.pathParameters['id'] ?? '',
),
),
),
StatefulShellRoute.indexedStack(
@ -101,8 +120,11 @@ class AppRoutes {
routes: [
GoRoute(
path: quips,
builder: (_, state) => QuipsFeedScreen(
initialPostId: state.uri.queryParameters['postId'],
builder: (_, state) => _deferred(
quips_feed_lib.loadLibrary,
() => quips_feed_lib.QuipsFeedScreen(
initialPostId: state.uri.queryParameters['postId'],
),
),
),
@ -112,7 +134,10 @@ class AppRoutes {
routes: [
GoRoute(
path: '/beacon',
builder: (_, __) => const BeaconScreen(),
builder: (_, __) => _deferred(
beacon_lib.loadLibrary,
() => beacon_lib.BeaconScreen(),
),
),
],
),
@ -120,11 +145,17 @@ class AppRoutes {
routes: [
GoRoute(
path: profile,
builder: (_, __) => const UnifiedProfileScreen(),
builder: (_, __) => _deferred(
profile_lib.loadLibrary,
() => profile_lib.UnifiedProfileScreen(),
),
routes: [
GoRoute(
path: 'blocked',
builder: (_, __) => const BlockedUsersScreen(),
builder: (_, __) => _deferred(
blocked_lib.loadLibrary,
() => blocked_lib.BlockedUsersScreen(),
),
),
],
),
@ -259,3 +290,27 @@ class AuthRefreshNotifier extends ChangeNotifier {
super.dispose();
}
}
// Deferred loading helper
/// Wraps a deferred import in a FutureBuilder that shows a loading
/// spinner until the JS chunk is fetched, then builds the real screen.
Widget _deferred(Future<void> Function() load, Widget Function() build) {
return FutureBuilder<void>(
future: load(),
builder: (_, snap) {
if (snap.connectionState != ConnectionState.done) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (snap.hasError) {
return const Scaffold(
body: Center(
child: Text('Failed to load. Please go back and try again.'),
),
);
}
return build();
},
);
}

View file

@ -1,25 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="Sojorn - Friend's Only. A product of MPLS LLC.">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#0A1128">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
@ -30,14 +18,65 @@
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<!-- Preconnect to CDN origins for faster sub-resource fetching -->
<link rel="preconnect" href="https://www.gstatic.com" crossorigin>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<title>sojorn</title>
<link rel="manifest" href="manifest.json">
<!-- Pre-Flutter loading screen (renders before any JS executes) -->
<style>
body { margin: 0; padding: 0; }
#loading {
position: fixed; inset: 0; z-index: 9999;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
background: #0A1128;
transition: opacity .3s ease;
}
#loading .logo {
width: 64px; height: 64px;
border-radius: 16px;
margin-bottom: 24px;
}
#loading .spinner {
width: 28px; height: 28px;
border: 3px solid rgba(255,255,255,.15);
border-top-color: #4A90D9;
border-radius: 50%;
animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
<script src="https://www.gstatic.com/firebasejs/10.7.0/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging-compat.js"></script>
</head>
<body>
<!-- Shown instantly while Flutter engine + app JS loads -->
<div id="loading">
<img class="logo" src="icons/Icon-192.png" alt="sojorn">
<div class="spinner"></div>
</div>
<script src="flutter_bootstrap.js" async></script>
<script>
// Remove loading screen once Flutter's glass pane is in the DOM
(function() {
var observer = new MutationObserver(function() {
if (document.querySelector('flt-glass-pane')) {
var el = document.getElementById('loading');
if (el) {
el.style.opacity = '0';
setTimeout(function() { el.remove(); }, 300);
}
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/firebase-messaging-sw.js');
}