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:
parent
6c0de3b439
commit
96468521cf
BIN
media/facebook/banner.png
Normal file
BIN
media/facebook/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
|
|
@ -4,24 +4,28 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
import '../services/api_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_dashboard_screen.dart';
|
||||||
import '../screens/admin/admin_scaffold.dart';
|
import '../screens/admin/admin_scaffold.dart';
|
||||||
import '../screens/admin/admin_user_base_screen.dart';
|
import '../screens/admin/admin_user_base_screen.dart';
|
||||||
import '../screens/admin/moderation_queue_screen.dart';
|
import '../screens/admin/moderation_queue_screen.dart';
|
||||||
import '../screens/admin/admin_content_tools_screen.dart';
|
import '../screens/admin/admin_content_tools_screen.dart';
|
||||||
import '../screens/beacon/beacon_screen.dart';
|
|
||||||
import '../screens/home/feed_personal_screen.dart';
|
// ── Deferred (code-split for web) ─────────────────────────────────────
|
||||||
import '../screens/home/home_shell.dart';
|
import '../screens/beacon/beacon_screen.dart' deferred as beacon_lib;
|
||||||
import '../screens/quips/create/quip_creation_flow.dart';
|
import '../screens/quips/create/quip_creation_flow.dart' deferred as quip_create_lib;
|
||||||
import '../screens/quips/feed/quips_feed_screen.dart';
|
import '../screens/quips/feed/quips_feed_screen.dart' deferred as quips_feed_lib;
|
||||||
import '../screens/profile/viewable_profile_screen.dart';
|
import '../screens/profile/viewable_profile_screen.dart' deferred as profile_lib;
|
||||||
import '../screens/profile/blocked_users_screen.dart';
|
import '../screens/profile/blocked_users_screen.dart' deferred as blocked_lib;
|
||||||
import '../screens/auth/auth_gate.dart';
|
import '../screens/secure_chat/secure_chat_full_screen.dart' deferred as secure_chat_lib;
|
||||||
import '../screens/discover/discover_screen.dart';
|
import '../screens/secure_chat/secure_chat_loader_screen.dart' deferred as chat_loader_lib;
|
||||||
import '../screens/secure_chat/secure_chat_full_screen.dart';
|
import '../screens/post/threaded_conversation_screen.dart' deferred as threaded_lib;
|
||||||
import '../screens/secure_chat/secure_chat_loader_screen.dart';
|
|
||||||
import '../screens/post/threaded_conversation_screen.dart';
|
|
||||||
import '../screens/notifications/notifications_screen.dart';
|
|
||||||
|
|
||||||
/// App routing config (GoRouter).
|
/// App routing config (GoRouter).
|
||||||
class AppRoutes {
|
class AppRoutes {
|
||||||
|
|
@ -54,36 +58,51 @@ class AppRoutes {
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '$userPrefix/:username',
|
path: '$userPrefix/:username',
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
builder: (_, state) => UnifiedProfileScreen(
|
builder: (_, state) => _deferred(
|
||||||
|
profile_lib.loadLibrary,
|
||||||
|
() => profile_lib.UnifiedProfileScreen(
|
||||||
handle: state.pathParameters['username'] ?? '',
|
handle: state.pathParameters['username'] ?? '',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: quipCreate,
|
path: quipCreate,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
builder: (_, __) => const QuipCreationFlow(),
|
builder: (_, __) => _deferred(
|
||||||
|
quip_create_lib.loadLibrary,
|
||||||
|
() => quip_create_lib.QuipCreationFlow(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: secureChat,
|
path: secureChat,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
builder: (_, __) => const SecureChatFullScreen(),
|
builder: (_, __) => _deferred(
|
||||||
|
secure_chat_lib.loadLibrary,
|
||||||
|
() => secure_chat_lib.SecureChatFullScreen(),
|
||||||
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: ':id',
|
path: ':id',
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
builder: (_, state) => SecureChatLoaderScreen(
|
builder: (_, state) => _deferred(
|
||||||
|
chat_loader_lib.loadLibrary,
|
||||||
|
() => chat_loader_lib.SecureChatLoaderScreen(
|
||||||
conversationId: state.pathParameters['id'] ?? '',
|
conversationId: state.pathParameters['id'] ?? '',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '$postPrefix/:id',
|
path: '$postPrefix/:id',
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
builder: (_, state) => ThreadedConversationScreen(
|
builder: (_, state) => _deferred(
|
||||||
|
threaded_lib.loadLibrary,
|
||||||
|
() => threaded_lib.ThreadedConversationScreen(
|
||||||
rootPostId: state.pathParameters['id'] ?? '',
|
rootPostId: state.pathParameters['id'] ?? '',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
StatefulShellRoute.indexedStack(
|
StatefulShellRoute.indexedStack(
|
||||||
builder: (context, state, navigationShell) => AuthGate(
|
builder: (context, state, navigationShell) => AuthGate(
|
||||||
authenticatedChild: HomeShell(navigationShell: navigationShell),
|
authenticatedChild: HomeShell(navigationShell: navigationShell),
|
||||||
|
|
@ -101,10 +120,13 @@ class AppRoutes {
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: quips,
|
path: quips,
|
||||||
builder: (_, state) => QuipsFeedScreen(
|
builder: (_, state) => _deferred(
|
||||||
|
quips_feed_lib.loadLibrary,
|
||||||
|
() => quips_feed_lib.QuipsFeedScreen(
|
||||||
initialPostId: state.uri.queryParameters['postId'],
|
initialPostId: state.uri.queryParameters['postId'],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -112,7 +134,10 @@ class AppRoutes {
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/beacon',
|
path: '/beacon',
|
||||||
builder: (_, __) => const BeaconScreen(),
|
builder: (_, __) => _deferred(
|
||||||
|
beacon_lib.loadLibrary,
|
||||||
|
() => beacon_lib.BeaconScreen(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -120,11 +145,17 @@ class AppRoutes {
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: profile,
|
path: profile,
|
||||||
builder: (_, __) => const UnifiedProfileScreen(),
|
builder: (_, __) => _deferred(
|
||||||
|
profile_lib.loadLibrary,
|
||||||
|
() => profile_lib.UnifiedProfileScreen(),
|
||||||
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'blocked',
|
path: 'blocked',
|
||||||
builder: (_, __) => const BlockedUsersScreen(),
|
builder: (_, __) => _deferred(
|
||||||
|
blocked_lib.loadLibrary,
|
||||||
|
() => blocked_lib.BlockedUsersScreen(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -259,3 +290,27 @@ class AuthRefreshNotifier extends ChangeNotifier {
|
||||||
super.dispose();
|
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();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,13 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<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">
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="Sojorn - Friend's Only. A product of MPLS LLC.">
|
<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="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="theme-color" content="#0A1128">
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
|
@ -30,14 +18,65 @@
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<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>
|
<title>sojorn</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<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-app-compat.js"></script>
|
||||||
<script src="https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging-compat.js"></script>
|
<script src="https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging-compat.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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 src="flutter_bootstrap.js" async></script>
|
||||||
<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) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register('/firebase-messaging-sw.js');
|
navigator.serviceWorker.register('/firebase-messaging-sw.js');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue