Improve page load speed

This commit is contained in:
DariusIII
2026-02-12 11:54:44 +01:00
parent dc8adf2ce7
commit e6028dbcbe
18 changed files with 211 additions and 101 deletions
+8 -2
View File
@@ -27,8 +27,14 @@ class AppServiceProvider extends ServiceProvider
{
Paginator::useTailwind();
// Share global data with all views using View Composer
view()->composer('*', \App\View\Composers\GlobalDataComposer::class);
// Share global data only with layouts and partials that need it
// (avoids running queries for emails, components, and other minimal views)
view()->composer([
'layouts.main',
'layouts.admin',
'layouts.guest',
'layouts.app',
], \App\View\Composers\GlobalDataComposer::class);
Gate::define('viewPulse', function (User $user) {
return $user->hasRole('Admin');
+35 -23
View File
@@ -8,50 +8,62 @@ use App\Models\Content;
use App\Models\Settings;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class GlobalDataComposer
{
/**
* Cache TTL in seconds (5 minutes).
*/
private const CACHE_TTL = 300;
/**
* Bind data to the view.
*/
public function compose(View $view): void
{
$viewName = $view->getName() ?? '';
$isEmailView = str_starts_with($viewName, 'emails.');
// Load settings as array with type conversions (empty strings -> null, numeric strings -> numbers)
$siteArray = Settings::query()
->pluck('value', 'name')
->map(fn ($value) => Settings::convertValue($value))
->all();
// Cached site settings (shared across all requests)
$siteArray = Cache::remember('site_settings_array', self::CACHE_TTL, function () {
return Settings::query()
->pluck('value', 'name')
->map(fn ($value) => Settings::convertValue($value))
->all();
});
$viewData = [
'serverroot' => url('/'),
'site' => $siteArray,
];
// Email views expect $site to be the string provided by the mailable
if (! $isEmailView) {
$viewData['site'] = $siteArray; // Now it's a proper array, not a Settings model
}
// Load useful links for sidebar
$usefulLinks = Content::active()
->ofType(Content::TYPE_USEFUL)
->orderBy('ordinal')
->get();
$viewData['usefulLinks'] = $usefulLinks;
// Cached useful links for sidebar
$viewData['usefulLinks'] = Cache::remember('content_useful_links', self::CACHE_TTL, function () {
return Content::active()
->ofType(Content::TYPE_USEFUL)
->orderBy('ordinal')
->get();
});
if (Auth::check()) {
$userdata = User::find(Auth::id());
$userdata->categoryexclusions = User::getCategoryExclusionById(Auth::id());
$userId = Auth::id();
// Update last login every 15 mins.
// User data with category exclusions (short cache per user)
$userdata = Cache::remember('composer_user_'.$userId, self::CACHE_TTL, function () use ($userId) {
$user = User::find($userId);
$user->categoryexclusions = User::getCategoryExclusionById($userId);
return $user;
});
// Update last login every 3 hours (outside of cache to avoid stale checks)
if (now()->subHours(3) > $userdata->lastlogin) {
event(new UserLoggedIn($userdata));
}
$parentcatlist = Category::getForMenu($userdata->categoryexclusions);
// Cached menu categories per user (depends on their exclusions)
$parentcatlist = Cache::remember('menu_user_'.$userId, self::CACHE_TTL, function () use ($userdata) {
return Category::getForMenu($userdata->categoryexclusions);
});
$viewData = array_merge($viewData, [
'userdata' => $userdata,
+13 -1
View File
@@ -185,6 +185,18 @@
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
],
"fix-style": "pint"
"fix-style": "pint",
"production-optimize": [
"@php artisan config:cache",
"@php artisan route:cache",
"@php artisan view:cache",
"@php artisan event:cache"
],
"production-clear": [
"@php artisan config:clear",
"@php artisan route:clear",
"@php artisan view:clear",
"@php artisan event:clear"
]
}
}
+5 -2
View File
@@ -15,8 +15,11 @@
--radius-card: 0.75rem;
}
/* Import FontAwesome */
@import '@fortawesome/fontawesome-free/css/all.min.css';
/* Import FontAwesome — only the weights we use (skips v4-compat font + shims) */
@import '@fortawesome/fontawesome-free/css/fontawesome.min.css';
@import '@fortawesome/fontawesome-free/css/solid.min.css';
@import '@fortawesome/fontawesome-free/css/regular.min.css';
@import '@fortawesome/fontawesome-free/css/brands.min.css';
/* Import custom CSP-safe styles */
@import './csp-safe.css';
+11 -30
View File
@@ -1,6 +1,10 @@
/**
* Alpine.js CSP-safe initialization
* Registers all stores and components, then starts Alpine.
*
* Core components are imported eagerly (used on every/most pages).
* Page-specific and heavy components are lazy-loaded: only the modules
* whose x-data names appear in the current page's DOM are fetched.
* This keeps the initial JS bundle small and improves TTI.
*/
import Alpine from '@alpinejs/csp';
@@ -12,7 +16,7 @@ import './stores/theme.js';
import './stores/toast.js';
import './stores/cart.js';
// --- Core UI components ---
// --- Core UI components (used on every/most pages) ---
import './components/theme-toggle.js';
import './components/back-to-top.js';
import './components/dropdown.js';
@@ -20,36 +24,12 @@ import './components/sidebar-toggle.js';
import './components/admin-submenu.js';
import './components/mobile-nav.js';
import './components/password-toggle.js';
// --- Modal components ---
import './components/confirm-modal.js';
import './components/nfo-modal.js';
import './components/image-modal.js';
import './components/preview-modal.js';
import './components/mediainfo-modal.js';
import './components/filelist-modal.js';
// --- Feature components ---
import './components/confirm-link.js';
import './components/toast-notification.js';
import './components/tab-switcher.js';
import './components/cart-button.js';
import './components/cart-page.js';
import './components/search-autocomplete.js';
import './components/quality-filter.js';
import './components/content-toggle.js';
import './components/movies-layout.js';
import './components/movies-page.js';
import './components/confirm-link.js';
// --- Page-specific components ---
import './components/release-report.js';
import './components/profile-edit.js';
import './components/auth-page.js';
import './components/toast-notification.js';
// --- Admin components ---
import './components/admin/dashboard.js';
import './components/admin/groups.js';
import './components/admin/features.js';
// --- Shared utility (backward compat) ---
window.escapeHtml = function(text) {
@@ -57,5 +37,6 @@ window.escapeHtml = function(text) {
return String(text).replace(/[&<>"']/g, m => map[m]);
};
// Start Alpine
Alpine.start();
// --- Lazy-load page-specific components, then start Alpine ---
import { loadAndStart } from './lazy-loader.js';
loadAndStart();
+89
View File
@@ -0,0 +1,89 @@
/**
* Alpine.js CSP-safe lazy component loader.
*
* Scans the DOM for x-data attributes that match known lazy components.
* Only imports the JS modules for components actually present on the page.
* After all needed modules are loaded, calls Alpine.start().
*
* Dynamic import() is CSP-compliant (no eval / new Function).
*/
import Alpine from '@alpinejs/csp';
/**
* Map of Alpine component name → dynamic import function.
* Each module registers itself via Alpine.data() as a side effect.
*/
const lazyComponentMap = {
// --- Modal components (only on release pages) ---
'nfoModal': () => import('./components/nfo-modal.js'),
'filelistModal': () => import('./components/filelist-modal.js'),
'previewModal': () => import('./components/preview-modal.js'),
'mediainfoModal': () => import('./components/mediainfo-modal.js'),
'imageModal': () => import('./components/image-modal.js'),
// --- Page-specific components ---
'moviesPage': () => import('./components/movies-page.js'),
'moviesLayout': () => import('./components/movies-layout.js'),
'qualityFilter': () => import('./components/quality-filter.js'),
'contentToggle': () => import('./components/content-toggle.js'),
'contentDelete': () => import('./components/content-toggle.js'), // same file
'releaseReport': () => import('./components/release-report.js'),
'adminReleaseReports': () => import('./components/release-report.js'), // same file
'profileEdit': () => import('./components/profile-edit.js'),
'profilePage': () => import('./components/profile-edit.js'), // same file
'copyToClipboard': () => import('./components/profile-edit.js'), // same file
'cartPage': () => import('./components/cart-page.js'),
'authPage': () => import('./components/auth-page.js'),
'otpInput': () => import('./components/auth-page.js'), // same file
// --- Admin components (includes Chart.js — heavy) ---
'adminDashboard': () => import('./components/admin/dashboard.js'),
'recentActivity': () => import('./components/admin/dashboard.js'), // same file
'adminGroups': () => import('./components/admin/groups.js'),
'adminFeatures': () => import('./components/admin/features.js'),
};
/**
* Extract Alpine component names from all x-data attributes in the DOM.
* Handles: x-data="componentName", x-data="componentName()", x-data="{ ... }" (skipped).
*/
function getUsedComponentNames() {
const names = new Set();
document.querySelectorAll('[x-data]').forEach(el => {
const raw = (el.getAttribute('x-data') || '').trim();
// Skip inline objects like { show: true }
if (!raw || raw.startsWith('{')) return;
// Extract identifier before ( or whitespace
const match = raw.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
if (match) names.add(match[1]);
});
return names;
}
/**
* Load only the lazy components found on the current page, then start Alpine.
*/
export function loadAndStart() {
const usedNames = getUsedComponentNames();
const imports = new Set(); // deduplicate (multiple names → same file)
for (const name of usedNames) {
if (lazyComponentMap[name]) {
imports.add(lazyComponentMap[name]);
}
}
if (imports.size === 0) {
Alpine.start();
return;
}
// Load all needed modules in parallel, then start Alpine
Promise.all([...imports].map(fn => fn()))
.then(() => Alpine.start())
.catch(err => {
console.error('[lazy-loader] Failed to load component:', err);
// Start Alpine anyway so the page is usable
Alpine.start();
});
}
+4
View File
@@ -1,5 +1,9 @@
@extends('layouts.main')
@push('modals')
@include('partials.release-modals')
@endpush
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm transition-colors duration-200">
<!-- Breadcrumb -->
+4
View File
@@ -1,5 +1,9 @@
@extends('layouts.main')
@push('modals')
@include('partials.release-modals')
@endpush
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
<!-- Breadcrumb -->
+4
View File
@@ -1,5 +1,9 @@
@extends('layouts.main')
@push('modals')
@include('partials.release-modals')
@endpush
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6">
<div class="mb-6">
+4
View File
@@ -1,5 +1,9 @@
@extends('layouts.main')
@push('modals')
@include('partials.release-modals')
@endpush
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
<!-- Breadcrumb -->
+9 -41
View File
@@ -100,59 +100,27 @@
@include('partials.back-to-top')
<!-- Theme Toggle -->
@php $themePreference = auth()->check() ? (auth()->user()->theme_preference ?? 'light') : 'light'; @endphp
<button id="theme-toggle" class="fixed z-50 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-4 py-3 rounded-full shadow-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-all duration-200 flex items-center gap-2 touch-target bottom-[max(1rem,env(safe-area-inset-bottom))] left-[max(1rem,env(safe-area-inset-left))]"
title="{{ ucfirst(auth()->check() ? (auth()->user()->theme_preference ?? 'light') : 'light') }} Mode">
<i id="theme-icon" class="fas
@if(auth()->check())
@if((auth()->user()->theme_preference ?? 'light') === 'dark')
fa-moon
@elseif((auth()->user()->theme_preference ?? 'light') === 'system')
fa-desktop
@else
fa-sun
@endif
@else
fa-sun
@endif
"></i>
<span id="theme-label" class="text-xs font-medium hidden sm:inline">
@if(auth()->check())
{{ ucfirst(auth()->user()->theme_preference ?? 'light') }}
@else
Light
@endif
</span>
title="{{ ucfirst($themePreference) }} Mode">
<i id="theme-icon" class="fas {{ $themePreference === 'dark' ? 'fa-moon' : ($themePreference === 'system' ? 'fa-desktop' : 'fa-sun') }}"></i>
<span id="theme-label" class="text-xs font-medium hidden sm:inline">{{ ucfirst($themePreference) }}</span>
</button>
<!-- Confirmation Modal -->
<!-- Confirmation Modal (used on many pages) -->
@include('partials.confirmation-modal')
<!-- File List Modal -->
@include('partials.filelist-modal')
<!-- NFO Modal -->
@include('partials.nfo-modal')
<!-- Preview/Sample Image Modal -->
@include('partials.preview-modal')
<!-- Media Info Modal -->
@include('partials.mediainfo-modal')
<!-- Image Modal -->
@include('partials.image-modal')
<!-- Shared Report Modal (single instance for all report-trigger buttons) -->
@include('partials.report-modal')
<!-- Toast Notifications (Alpine.js CSP Safe) -->
@include('partials.toast-notifications')
{{-- Release-specific modals: pushed by pages that show releases --}}
@stack('modals')
@stack('scripts')
<!-- Theme Management Data (moved to csp-safe.js) -->
<div id="current-theme-data"
data-theme="{{ auth()->check() ? (auth()->user()->theme_preference ?? 'light') : 'light' }}"
data-theme="{{ $themePreference }}"
data-authenticated="{{ auth()->check() ? 'true' : 'false' }}"
data-update-url="{{ route('profile.update-theme') }}"
style="display: none;">
+4
View File
@@ -1,5 +1,9 @@
@extends('layouts.main')
@push('modals')
@include('partials.release-modals')
@endpush
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm" x-data="moviesPage" data-movie-layout="{{ $movie_layout ?? 2 }}">
{{-- Breadcrumb --}}
+4
View File
@@ -1,5 +1,9 @@
@extends('layouts.main')
@push('modals')
@include('partials.release-modals')
@endpush
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
<!-- Breadcrumb -->
@@ -254,7 +254,7 @@
<i class="fa fa-user fa-fw mr-2"></i>Profile
</a>
<a href="{{ route('logout') }}" data-logout class="block px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white">
<i class="fa fa-sign-out fa-fw mr-2"></i>Sign Out
<i class="fas fa-sign-out-alt fa-fw mr-2"></i>Sign Out
</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="hidden">
@csrf
@@ -0,0 +1,7 @@
{{-- Release-related modals: only include on pages that show releases --}}
@include('partials.filelist-modal')
@include('partials.nfo-modal')
@include('partials.preview-modal')
@include('partials.mediainfo-modal')
@include('partials.image-modal')
@include('partials.report-modal')
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="sidebar-section">
<button class="sidebar-toggle w-full flex items-center justify-between px-4 py-3 text-white hover:bg-gray-800 rounded-lg transition" data-target="submenu1">
<div class="flex items-center">
<i class="fa fa-dashboard fa-fw mr-3"></i>
<i class="fas fa-tachometer-alt fa-fw mr-3"></i>
<span>Browse</span>
</div>
<i class="fas fa-chevron-down text-xs transition-transform"></i>
+4
View File
@@ -1,5 +1,9 @@
@extends('layouts.main')
@push('modals')
@include('partials.release-modals')
@endpush
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6">
<div class="mb-6">
+4
View File
@@ -1,5 +1,9 @@
@extends('layouts.main')
@push('modals')
@include('partials.release-modals')
@endpush
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
<!-- Breadcrumb -->