perf: optimize cold start by lazy-loading icons and parallelizing DB queries
- Replace barrel `import * as icons` in DynamicIcon with dynamic per-icon imports - Eagerly connect Prisma client at startup to avoid first-request latency - Parallelize 4 sequential DB queries in layout server load with Promise.all
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import * as icons from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
name: string | null;
|
||||
size?: number;
|
||||
@@ -9,17 +7,33 @@
|
||||
|
||||
let { name, size = 16, class: className = '' }: Props = $props();
|
||||
|
||||
// Convert kebab-case to PascalCase: "layout-dashboard" → "LayoutDashboard"
|
||||
function toPascalCase(str: string): string {
|
||||
// Convert PascalCase or kebab-case to kebab-case filename
|
||||
// "LayoutDashboard" → "layout-dashboard", "layout-dashboard" stays as-is
|
||||
function toKebabCase(str: string): string {
|
||||
return str
|
||||
.split('-')
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join('');
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
||||
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
const iconComponent = $derived(
|
||||
name ? ((icons as Record<string, unknown>)[toPascalCase(name)] as typeof import('svelte').SvelteComponent | undefined) ?? null : null
|
||||
);
|
||||
let iconComponent = $state<typeof import('svelte').SvelteComponent | null>(null);
|
||||
$effect(() => {
|
||||
const currentName = name;
|
||||
iconComponent = null;
|
||||
if (!currentName) return;
|
||||
|
||||
const kebab = toKebabCase(currentName);
|
||||
import(/* @vite-ignore */ `lucide-svelte/icons/${kebab}`)
|
||||
.then((mod) => {
|
||||
// Only apply if name hasn't changed during load
|
||||
if (name === currentName) {
|
||||
iconComponent = mod.default;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Icon not found
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if iconComponent}
|
||||
|
||||
@@ -7,3 +7,8 @@ export const prisma = globalForPrisma.prisma || new PrismaClient();
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
|
||||
// Eagerly connect so the first request doesn't pay the connection cost
|
||||
prisma.$connect().catch(() => {
|
||||
// Connection will be retried lazily on first query
|
||||
});
|
||||
|
||||
@@ -3,49 +3,20 @@ import { prisma } from '$lib/server/prisma.js';
|
||||
import { isOnboardingNeeded } from '$lib/server/services/onboardingService.js';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
// Fetch sidebar boards for the layout
|
||||
let boards: Array<{ id: string; name: string; icon: string | null }> = [];
|
||||
|
||||
try {
|
||||
if (locals.user) {
|
||||
// Authenticated user: fetch boards they can access
|
||||
if (locals.user.role === 'admin') {
|
||||
boards = await prisma.board.findMany({
|
||||
// Build all queries upfront and run them in parallel
|
||||
const boardsQuery = locals.user
|
||||
? prisma.board.findMany({
|
||||
select: { id: true, name: true, icon: true },
|
||||
orderBy: [{ isDefault: 'desc' }, { name: 'asc' }]
|
||||
});
|
||||
} else {
|
||||
// Regular users: fetch all boards (permission filtering done at page level)
|
||||
boards = await prisma.board.findMany({
|
||||
select: { id: true, name: true, icon: true },
|
||||
orderBy: [{ isDefault: 'desc' }, { name: 'asc' }]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Guest: only guest-accessible boards
|
||||
boards = await prisma.board.findMany({
|
||||
})
|
||||
: prisma.board.findMany({
|
||||
where: { isGuestAccessible: true },
|
||||
select: { id: true, name: true, icon: true },
|
||||
orderBy: [{ isDefault: 'desc' }, { name: 'asc' }]
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Fail gracefully — sidebar will just be empty
|
||||
boards = [];
|
||||
}
|
||||
|
||||
// Fetch user preferences if authenticated
|
||||
let userPreferences: {
|
||||
themeMode: string | null;
|
||||
primaryHue: number | null;
|
||||
primarySaturation: number | null;
|
||||
backgroundType: string | null;
|
||||
locale: string | null;
|
||||
} | null = null;
|
||||
|
||||
if (locals.user) {
|
||||
try {
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
const preferencesQuery = locals.user
|
||||
? prisma.user.findUnique({
|
||||
where: { id: locals.user.id },
|
||||
select: {
|
||||
themeMode: true,
|
||||
@@ -54,38 +25,28 @@ export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
backgroundType: true,
|
||||
locale: true
|
||||
}
|
||||
});
|
||||
userPreferences = dbUser ?? null;
|
||||
} catch {
|
||||
// Fail gracefully
|
||||
}
|
||||
}
|
||||
})
|
||||
: Promise.resolve(null);
|
||||
|
||||
// Fetch system-level custom CSS
|
||||
let systemCustomCss: string | null = null;
|
||||
try {
|
||||
const settings = await prisma.systemSettings.findUnique({
|
||||
const settingsQuery = prisma.systemSettings.findUnique({
|
||||
where: { id: 'singleton' },
|
||||
select: { customCss: true }
|
||||
});
|
||||
systemCustomCss = settings?.customCss ?? null;
|
||||
} catch {
|
||||
// Fail gracefully
|
||||
}
|
||||
|
||||
// Check if onboarding is needed
|
||||
let onboardingNeeded = false;
|
||||
try {
|
||||
onboardingNeeded = await isOnboardingNeeded();
|
||||
} catch {
|
||||
// Fail gracefully — don't block the app
|
||||
}
|
||||
const onboardingQuery = isOnboardingNeeded();
|
||||
|
||||
const [boards, userPreferences, settings, onboardingNeeded] = await Promise.all([
|
||||
boardsQuery.catch(() => [] as Array<{ id: string; name: string; icon: string | null }>),
|
||||
preferencesQuery.catch(() => null),
|
||||
settingsQuery.catch(() => null),
|
||||
onboardingQuery.catch(() => false)
|
||||
]);
|
||||
|
||||
return {
|
||||
user: locals.user,
|
||||
sidebarBoards: boards,
|
||||
userPreferences,
|
||||
systemCustomCss,
|
||||
systemCustomCss: settings?.customCss ?? null,
|
||||
onboardingNeeded
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user