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">
|
<script lang="ts">
|
||||||
import * as icons from 'lucide-svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string | null;
|
name: string | null;
|
||||||
size?: number;
|
size?: number;
|
||||||
@@ -9,17 +7,33 @@
|
|||||||
|
|
||||||
let { name, size = 16, class: className = '' }: Props = $props();
|
let { name, size = 16, class: className = '' }: Props = $props();
|
||||||
|
|
||||||
// Convert kebab-case to PascalCase: "layout-dashboard" → "LayoutDashboard"
|
// Convert PascalCase or kebab-case to kebab-case filename
|
||||||
function toPascalCase(str: string): string {
|
// "LayoutDashboard" → "layout-dashboard", "layout-dashboard" stays as-is
|
||||||
|
function toKebabCase(str: string): string {
|
||||||
return str
|
return str
|
||||||
.split('-')
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
||||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
||||||
.join('');
|
.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconComponent = $derived(
|
let iconComponent = $state<typeof import('svelte').SvelteComponent | null>(null);
|
||||||
name ? ((icons as Record<string, unknown>)[toPascalCase(name)] as typeof import('svelte').SvelteComponent | undefined) ?? 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>
|
</script>
|
||||||
|
|
||||||
{#if iconComponent}
|
{#if iconComponent}
|
||||||
|
|||||||
@@ -7,3 +7,8 @@ export const prisma = globalForPrisma.prisma || new PrismaClient();
|
|||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
globalForPrisma.prisma = prisma;
|
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';
|
import { isOnboardingNeeded } from '$lib/server/services/onboardingService.js';
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||||
// Fetch sidebar boards for the layout
|
// Build all queries upfront and run them in parallel
|
||||||
let boards: Array<{ id: string; name: string; icon: string | null }> = [];
|
const boardsQuery = locals.user
|
||||||
|
? prisma.board.findMany({
|
||||||
try {
|
select: { id: true, name: true, icon: true },
|
||||||
if (locals.user) {
|
orderBy: [{ isDefault: 'desc' }, { name: 'asc' }]
|
||||||
// Authenticated user: fetch boards they can access
|
})
|
||||||
if (locals.user.role === 'admin') {
|
: prisma.board.findMany({
|
||||||
boards = await 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({
|
|
||||||
where: { isGuestAccessible: true },
|
where: { isGuestAccessible: true },
|
||||||
select: { id: true, name: true, icon: true },
|
select: { id: true, name: true, icon: true },
|
||||||
orderBy: [{ isDefault: 'desc' }, { name: 'asc' }]
|
orderBy: [{ isDefault: 'desc' }, { name: 'asc' }]
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Fail gracefully — sidebar will just be empty
|
|
||||||
boards = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch user preferences if authenticated
|
const preferencesQuery = locals.user
|
||||||
let userPreferences: {
|
? prisma.user.findUnique({
|
||||||
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({
|
|
||||||
where: { id: locals.user.id },
|
where: { id: locals.user.id },
|
||||||
select: {
|
select: {
|
||||||
themeMode: true,
|
themeMode: true,
|
||||||
@@ -54,38 +25,28 @@ export const load: LayoutServerLoad = async ({ locals }) => {
|
|||||||
backgroundType: true,
|
backgroundType: true,
|
||||||
locale: true
|
locale: true
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
userPreferences = dbUser ?? null;
|
: Promise.resolve(null);
|
||||||
} catch {
|
|
||||||
// Fail gracefully
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch system-level custom CSS
|
const settingsQuery = prisma.systemSettings.findUnique({
|
||||||
let systemCustomCss: string | null = null;
|
where: { id: 'singleton' },
|
||||||
try {
|
select: { customCss: true }
|
||||||
const settings = await prisma.systemSettings.findUnique({
|
});
|
||||||
where: { id: 'singleton' },
|
|
||||||
select: { customCss: true }
|
|
||||||
});
|
|
||||||
systemCustomCss = settings?.customCss ?? null;
|
|
||||||
} catch {
|
|
||||||
// Fail gracefully
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if onboarding is needed
|
const onboardingQuery = isOnboardingNeeded();
|
||||||
let onboardingNeeded = false;
|
|
||||||
try {
|
const [boards, userPreferences, settings, onboardingNeeded] = await Promise.all([
|
||||||
onboardingNeeded = await isOnboardingNeeded();
|
boardsQuery.catch(() => [] as Array<{ id: string; name: string; icon: string | null }>),
|
||||||
} catch {
|
preferencesQuery.catch(() => null),
|
||||||
// Fail gracefully — don't block the app
|
settingsQuery.catch(() => null),
|
||||||
}
|
onboardingQuery.catch(() => false)
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: locals.user,
|
user: locals.user,
|
||||||
sidebarBoards: boards,
|
sidebarBoards: boards,
|
||||||
userPreferences,
|
userPreferences,
|
||||||
systemCustomCss,
|
systemCustomCss: settings?.customCss ?? null,
|
||||||
onboardingNeeded
|
onboardingNeeded
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user