Skip to content

Preact Version โ€‹

HaloLight Preact version is built on Preact + Vite, using Signals + TypeScript to deliver a lightweight, high-performance admin dashboard.

Live Preview: https://halolight-preact.h7ml.cn

GitHub: https://github.com/halolight/halolight-preact

Features โ€‹

  • ๐Ÿชถ Ultra Lightweight - Core library only 3KB gzip
  • โšก High-Performance Signals - Reactive state management with automatic dependency tracking
  • ๐ŸŽจ Theme System - 11 skins, dark/light mode, View Transitions
  • ๐Ÿ” Authentication System - Complete login/register/password recovery flow
  • ๐Ÿ“Š Dashboard - Data visualization and business management
  • ๐Ÿ›ก๏ธ Permission Control - RBAC fine-grained permission management
  • โš›๏ธ React Compatible - Can directly use most React ecosystem libraries
  • ๐Ÿš€ Fast Startup - Vite provides ultra-fast dev experience

Tech Stack โ€‹

TechnologyVersionDescription
Preact10.xLightweight React alternative
@preact/signals2.xReactive state management
TypeScript5.9Type safety
Tailwind CSS4.xAtomic CSS
shadcn/uilatestUI component library (compat layer)
Vite7.2Build tool
preact-router4.xClient-side routing
TanStack Query5.xServer state
Mock.js1.xData mocking

Core Features โ€‹

  • Signals State Management - High-performance reactive, automatic dependency tracking, fine-grained updates
  • Permission System - RBAC permission control, route guards, permission components
  • Theme System - 11 skins, dark/light mode, View Transitions
  • Data Mocking - Mock.js + Fetch interception, complete backend simulation
  • React Compatible - Use React ecosystem libraries through preact/compat

Directory Structure โ€‹

halolight-preact/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ pages/                     # Page components
โ”‚   โ”‚   โ”œโ”€โ”€ Home.tsx              # Homepage
โ”‚   โ”‚   โ”œโ”€โ”€ auth/                 # Auth pages
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Login.tsx
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Register.tsx
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ ForgotPassword.tsx
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ ResetPassword.tsx
โ”‚   โ”‚   โ””โ”€โ”€ dashboard/            # Dashboard pages
โ”‚   โ”‚       โ”œโ”€โ”€ Dashboard.tsx
โ”‚   โ”‚       โ”œโ”€โ”€ Users.tsx
โ”‚   โ”‚       โ”œโ”€โ”€ UserDetail.tsx
โ”‚   โ”‚       โ”œโ”€โ”€ UserCreate.tsx
โ”‚   โ”‚       โ”œโ”€โ”€ Roles.tsx
โ”‚   โ”‚       โ”œโ”€โ”€ Permissions.tsx
โ”‚   โ”‚       โ”œโ”€โ”€ Settings.tsx
โ”‚   โ”‚       โ””โ”€โ”€ Profile.tsx
โ”‚   โ”œโ”€โ”€ components/               # Component library
โ”‚   โ”‚   โ”œโ”€โ”€ ui/                   # UI components
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Button.tsx
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Input.tsx
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Card.tsx
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ Dialog.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ layout/               # Layout components
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ AdminLayout.tsx
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ AuthLayout.tsx
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Sidebar.tsx
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ Header.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ dashboard/            # Dashboard components
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ DashboardGrid.tsx
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ WidgetWrapper.tsx
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ StatsWidget.tsx
โ”‚   โ”‚   โ””โ”€โ”€ shared/               # Shared components
โ”‚   โ”‚       โ””โ”€โ”€ PermissionGuard.tsx
โ”‚   โ”œโ”€โ”€ stores/                   # State management
โ”‚   โ”‚   โ”œโ”€โ”€ auth.ts
โ”‚   โ”‚   โ”œโ”€โ”€ ui-settings.ts
โ”‚   โ”‚   โ””โ”€โ”€ dashboard.ts
โ”‚   โ”œโ”€โ”€ hooks/                    # Custom Hooks
โ”‚   โ”‚   โ”œโ”€โ”€ useAuth.ts
โ”‚   โ”‚   โ””โ”€โ”€ usePermission.ts
โ”‚   โ”œโ”€โ”€ lib/                      # Utilities
โ”‚   โ”‚   โ”œโ”€โ”€ api.ts
โ”‚   โ”‚   โ”œโ”€โ”€ permission.ts
โ”‚   โ”‚   โ””โ”€โ”€ cn.ts
โ”‚   โ”œโ”€โ”€ mock/                     # Mock data
โ”‚   โ”‚   โ”œโ”€โ”€ index.ts
โ”‚   โ”‚   โ””โ”€โ”€ handlers/
โ”‚   โ”œโ”€โ”€ types/                    # Type definitions
โ”‚   โ”œโ”€โ”€ App.tsx                   # Root component
โ”‚   โ”œโ”€โ”€ routes.tsx                # Route config
โ”‚   โ””โ”€โ”€ main.tsx                  # Entry file
โ”œโ”€โ”€ public/                       # Static assets
โ”œโ”€โ”€ vite.config.ts               # Vite config
โ”œโ”€โ”€ tailwind.config.ts           # Tailwind config
โ””โ”€โ”€ package.json

Quick Start โ€‹

Requirements โ€‹

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation โ€‹

bash
git clone https://github.com/halolight/halolight-preact.git
cd halolight-preact
pnpm install

Environment Variables โ€‹

bash
cp .env.example .env
env
# .env
VITE_API_URL=/api
VITE_USE_MOCK=true
VITE_DEMO_EMAIL=admin@halolight.h7ml.cn
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

Start Development โ€‹

bash
pnpm dev

Visit http://localhost:5173

Production Build โ€‹

bash
pnpm build
pnpm preview

Core Features โ€‹

State Management (@preact/signals) โ€‹

tsx
// stores/auth.ts
import { signal, computed, effect } from '@preact/signals'

interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

// Reactive state
export const user = signal<User | null>(null)
export const token = signal<string | null>(null)
export const loading = signal(false)

// Computed properties
export const isAuthenticated = computed(() => !!token.value && !!user.value)
export const permissions = computed(() => user.value?.permissions ?? [])

// Persistence
effect(() => {
  if (user.value && token.value) {
    localStorage.setItem('auth', JSON.stringify({
      user: user.value,
      token: token.value,
    }))
  }
})

// Initialization
const saved = localStorage.getItem('auth')
if (saved) {
  const { user: savedUser, token: savedToken } = JSON.parse(saved)
  user.value = savedUser
  token.value = savedToken
}

// Methods
export async function login(credentials: { email: string; password: string }) {
  loading.value = true
  try {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
      headers: { 'Content-Type': 'application/json' },
    })
    const data = await response.json()

    user.value = data.user
    token.value = data.token
  } finally {
    loading.value = false
  }
}

export function logout() {
  user.value = null
  token.value = null
  localStorage.removeItem('auth')
}

export function hasPermission(permission: string): boolean {
  const perms = permissions.value
  return perms.some(p =>
    p === '*' || p === permission ||
    (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
  )
}

Signals Features:

  • Fine-grained Updates - Only updates components that depend on the Signal
  • Automatic Dependency Tracking - No need to manually declare dependencies
  • No Memoization Needed - Computed properties are automatically cached
  • Cross-component Communication - Global state automatically syncs

Data Fetching (TanStack Query) โ€‹

tsx
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { token } from '../stores/auth'

export function useUsers(page = 1) {
  return useQuery({
    queryKey: ['users', page],
    queryFn: async () => {
      const response = await fetch(`/api/users?page=${page}`, {
        headers: { Authorization: `Bearer ${token.value}` },
      })
      return response.json()
    },
    enabled: !!token.value,
  })
}

export function useCreateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async (data: CreateUserDto) => {
      const response = await fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(data),
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${token.value}`,
        },
      })
      return response.json()
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })
}
tsx
// Usage
import { useUsers } from '../hooks/useUsers'

export function UsersPage() {
  const { data, isLoading, error } = useUsers(1)

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Failed to load</div>

  return (
    <ul>
      {data.data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Permission Control โ€‹

tsx
// hooks/usePermission.ts
import { hasPermission } from '../stores/auth'

export function usePermission() {
  return {
    hasPermission,
    can: (permission: string) => hasPermission(permission),
  }
}
tsx
// components/shared/PermissionGuard.tsx
import { ComponentChildren } from 'preact'
import { hasPermission } from '../../stores/auth'

interface Props {
  permission: string
  children: ComponentChildren
  fallback?: ComponentChildren
}

export function PermissionGuard({ permission, children, fallback }: Props) {
  if (!hasPermission(permission)) {
    return fallback ?? null
  }

  return children
}
tsx
// Usage
<PermissionGuard
  permission="users:delete"
  fallback={<span class="text-muted-foreground">No permission</span>}
>
  <Button variant="destructive">Delete</Button>
</PermissionGuard>

Route Configuration โ€‹

tsx
// routes.tsx
import Router, { Route } from 'preact-router'
import { isAuthenticated, hasPermission } from './stores/auth'

// Page components
import Home from './pages/Home'
import Login from './pages/auth/Login'
import Register from './pages/auth/Register'
import Dashboard from './pages/dashboard/Dashboard'
import Users from './pages/dashboard/Users'

// Route guard HOC
function ProtectedRoute({ component: Component, permission, ...rest }) {
  if (!isAuthenticated.value) {
    route('/login?redirect=' + rest.path)
    return null
  }

  if (permission && !hasPermission(permission)) {
    return <div>No permission</div>
  }

  return <Component {...rest} />
}

export function AppRouter() {
  return (
    <Router>
      <Route path="/" component={Home} />
      <Route path="/login" component={Login} />
      <Route path="/register" component={Register} />
      <ProtectedRoute
        path="/dashboard"
        component={Dashboard}
        permission="dashboard:view"
      />
      <ProtectedRoute
        path="/users"
        component={Users}
        permission="users:list"
      />
    </Router>
  )
}

Theme System โ€‹

Skin Presets โ€‹

Support 11 preset skins, switch via quick settings panel:

SkinColorCSS Variable
DefaultPurple--primary: 51.1% 0.262 276.97
BlueBlue--primary: 54.8% 0.243 264.05
EmeraldEmerald--primary: 64.6% 0.178 142.49
AmberAmber--primary: 78.3% 0.177 74.21
RoseRose--primary: 62.8% 0.243 12.48
SlateSlate--primary: 51.4% 0.032 257.42
ZincZinc--primary: 50.7% 0.017 285.96
StoneStone--primary: 53.4% 0.015 69.82
NeutralNeutral--primary: 50.9% 0.016 286.13
RedRed--primary: 55.5% 0.238 25.33
OrangeOrange--primary: 72.3% 0.187 56.24

CSS Variables (OKLch) โ€‹

css
/* Light mode */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  --secondary: 96.5% 0.006 286.32;
  --secondary-foreground: 21.7% 0.026 285.88;
  --accent: 96.5% 0.006 286.32;
  --accent-foreground: 21.7% 0.026 285.88;
}

/* Dark mode */
.dark {
  --background: 15.5% 0.018 285.88;
  --foreground: 98.3% 0.006 286.32;
  --primary: 74.1% 0.196 275.74;
  --primary-foreground: 21.7% 0.043 286.07;
  --secondary: 20.7% 0.021 286.05;
  --secondary-foreground: 98.3% 0.006 286.32;
}

Theme Switching โ€‹

tsx
// stores/ui-settings.ts
import { signal, effect } from '@preact/signals'

export const theme = signal<'light' | 'dark'>('light')
export const skin = signal<string>('default')

// Persistence
effect(() => {
  localStorage.setItem('theme', theme.value)
  document.documentElement.classList.toggle('dark', theme.value === 'dark')
})

effect(() => {
  localStorage.setItem('skin', skin.value)
  document.documentElement.dataset.skin = skin.value
})

// Initialization
theme.value = (localStorage.getItem('theme') as any) || 'light'
skin.value = localStorage.getItem('skin') || 'default'

export function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

export function setSkin(newSkin: string) {
  skin.value = newSkin
}

Page Routes โ€‹

PathPagePermission
/HomepagePublic
/loginLoginPublic
/registerRegisterPublic
/forgot-passwordForgot PasswordPublic
/reset-passwordReset PasswordPublic
/dashboardDashboarddashboard:view
/usersUser Listusers:list
/users/createCreate Userusers:create
/users/:idUser Detailusers:view
/rolesRole Managementroles:list
/permissionsPermission Managementpermissions:list
/settingsSystem Settingssettings:view
/profileProfileLogged in

Common Commands โ€‹

bash
pnpm dev            # Start dev server
pnpm build          # Production build
pnpm preview        # Preview production build
pnpm lint           # Code linting
pnpm lint:fix       # Auto fix
pnpm type-check     # Type checking
pnpm test           # Run tests
pnpm test:coverage  # Test coverage

Deployment โ€‹

Deploy with Vercel

Docker โ€‹

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
bash
docker build -t halolight-preact .
docker run -p 80:80 halolight-preact

Other Platforms โ€‹

Demo Accounts โ€‹

RoleEmailPassword
Adminadmin@halolight.h7ml.cn123456
Useruser@halolight.h7ml.cn123456

Testing โ€‹

Test Commands โ€‹

bash
pnpm test           # Run tests (watch mode)
pnpm test:run       # Single run
pnpm test:coverage  # Coverage report
pnpm test:ui        # Vitest UI

Test File Organization โ€‹

Test files are placed together with source files, using .test.ts or .test.tsx suffix:

src/components/ui/
โ”œโ”€โ”€ Button.tsx
โ”œโ”€โ”€ Button.test.tsx     # Button component test
โ”œโ”€โ”€ Input.tsx
โ””โ”€โ”€ Input.test.tsx      # Input component test

Test Example โ€‹

tsx
// src/components/ui/Button.test.tsx
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/preact'
import { Button } from './Button'

describe('Button', () => {
  it('renders default button', () => {
    render(<Button>Click</Button>)
    expect(screen.getByRole('button')).toHaveTextContent('Click')
  })

  it('renders different variants', () => {
    render(<Button variant="destructive">Delete</Button>)
    expect(screen.getByRole('button')).toHaveClass('bg-destructive')
  })

  it('handles disabled state', () => {
    render(<Button disabled>Disabled</Button>)
    expect(screen.getByRole('button')).toBeDisabled()
  })
})

Configuration โ€‹

Vite Configuration โ€‹

ts
// vite.config.ts
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'
import path from 'path'

export default defineConfig({
  plugins: [preact()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      // React compatibility
      react: 'preact/compat',
      'react-dom': 'preact/compat',
      'react/jsx-runtime': 'preact/jsx-runtime',
    },
  },
  build: {
    target: 'esnext',
    minify: 'terser',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['preact', 'preact/hooks'],
          router: ['preact-router'],
          query: ['@tanstack/react-query'],
        },
      },
    },
  },
})

Tailwind Configuration โ€‹

ts
// tailwind.config.ts
import type { Config } from 'tailwindcss'

export default {
  darkMode: ['class'],
  content: [
    './index.html',
    './src/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        border: 'oklch(var(--border) / <alpha-value>)',
        input: 'oklch(var(--input) / <alpha-value>)',
        ring: 'oklch(var(--ring) / <alpha-value>)',
        background: 'oklch(var(--background) / <alpha-value>)',
        foreground: 'oklch(var(--foreground) / <alpha-value>)',
        primary: {
          DEFAULT: 'oklch(var(--primary) / <alpha-value>)',
          foreground: 'oklch(var(--primary-foreground) / <alpha-value>)',
        },
      },
    },
  },
  plugins: [require('tailwindcss-animate')],
} satisfies Config

CI/CD โ€‹

The project is configured with complete GitHub Actions CI workflow:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features โ€‹

Component Example โ€‹

tsx
// components/ui/Button.tsx
import { ComponentChildren } from 'preact'
import { cn } from '../../lib/cn'

interface Props {
  variant?: 'default' | 'destructive' | 'outline' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  class?: string
  children: ComponentChildren
  onClick?: () => void
}

export function Button({
  variant = 'default',
  size = 'md',
  disabled,
  class: className,
  children,
  onClick,
}: Props) {
  return (
    <button
      class={cn(
        'inline-flex items-center justify-center rounded-md font-medium transition-colors',
        {
          'bg-primary text-primary-foreground hover:bg-primary/90':
            variant === 'default',
          'bg-destructive text-destructive-foreground hover:bg-destructive/90':
            variant === 'destructive',
          'border border-input bg-background hover:bg-accent':
            variant === 'outline',
          'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
          'h-8 px-3 text-sm': size === 'sm',
          'h-10 px-4': size === 'md',
          'h-12 px-6 text-lg': size === 'lg',
          'opacity-50 cursor-not-allowed': disabled,
        },
        className
      )}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

Form Handling โ€‹

tsx
// pages/auth/Login.tsx
import { useState } from 'preact/hooks'
import { route } from 'preact-router'
import { login, loading } from '../../stores/auth'

export default function LoginPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState('')

  const handleSubmit = async (e: Event) => {
    e.preventDefault()
    setError('')

    try {
      await login({ email, password })
      const params = new URLSearchParams(location.search)
      route(params.get('redirect') || '/dashboard')
    } catch (e) {
      setError('Invalid email or password')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {error && <div class="text-destructive">{error}</div>}

      <input
        type="email"
        value={email}
        onInput={(e) => setEmail(e.currentTarget.value)}
        placeholder="Email"
        required
      />

      <input
        type="password"
        value={password}
        onInput={(e) => setPassword(e.currentTarget.value)}
        placeholder="Password"
        required
      />

      <button type="submit" disabled={loading.value}>
        {loading.value ? 'Logging in...' : 'Login'}
      </button>
    </form>
  )
}

Performance Optimization โ€‹

Lazy Loading Components โ€‹

tsx
// App.tsx
import { lazy, Suspense } from 'preact/compat'

const Dashboard = lazy(() => import('./pages/dashboard/Dashboard'))
const Users = lazy(() => import('./pages/dashboard/Users'))

export function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Router>
        <Route path="/dashboard" component={Dashboard} />
        <Route path="/users" component={Users} />
      </Router>
    </Suspense>
  )
}

Code Splitting โ€‹

ts
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['preact', 'preact/hooks'],
          router: ['preact-router'],
          query: ['@tanstack/react-query'],
        },
      },
    },
  },
})

Signals Optimization โ€‹

tsx
// Use computed to avoid redundant calculations
import { signal, computed } from '@preact/signals'

const items = signal([1, 2, 3, 4, 5])
const filter = signal('all')

// Computed properties are automatically cached
const filteredItems = computed(() => {
  if (filter.value === 'all') return items.value
  return items.value.filter(item => item > 2)
})

// Usage in component
function ItemList() {
  return (
    <ul>
      {filteredItems.value.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  )
}

FAQ โ€‹

Q: How to use React ecosystem libraries? โ€‹

A: Preact provides React compatibility through preact/compat, most React libraries can be used directly:

ts
// vite.config.ts
export default defineConfig({
  resolve: {
    alias: {
      react: 'preact/compat',
      'react-dom': 'preact/compat',
      'react/jsx-runtime': 'preact/jsx-runtime',
    },
  },
})

Q: How do Signals work with React Hooks? โ€‹

A: Signals can be used directly in components without useState:

tsx
import { signal } from '@preact/signals'

const count = signal(0)

function Counter() {
  // Use signal.value directly
  return (
    <button onClick={() => count.value++}>
      Count: {count.value}
    </button>
  )
}

Q: How to optimize first screen loading? โ€‹

A: Use code splitting and lazy loading:

tsx
import { lazy, Suspense } from 'preact/compat'

const HeavyComponent = lazy(() => import('./HeavyComponent'))

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  )
}

Comparison with Other Versions โ€‹

FeaturePreactNext.jsVue
SSR/SSGโŒ (SPA)โœ…โœ… (Nuxt)
State ManagementSignalsZustandPinia
Routingpreact-routerApp RouterVue Router
Build ToolViteNext.jsVite
Bundle Size~3KB~85KB~33KB
React Compatibilityโœ…-โŒ
Learning CurveLowMediumMedium