Skip to content

Commit 32ed161

Browse files
committed
perf: optimize dashboard rendering paths
1 parent b668271 commit 32ed161

17 files changed

Lines changed: 714 additions & 256 deletions

README.md

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ Navet (Swedish for "the hub") is a modern, responsive smart home dashboard built
6060
- **Secure Connections** - Direct connection to your smart home instance
6161

6262
### ⚡ Performance
63-
- **Optimized Bundle** - 1.2MB (52% smaller after cleanup)
64-
- **28 Dependencies** - Removed 19 unused packages (-40%)
65-
- **Fast Loading** - ~400ms first load time
66-
- **Smart Re-renders** - Zustand selective subscriptions
63+
- **Lazy-Loaded UI** - Settings, add-card flows, widgets, and media dialogs load on demand
64+
- **Deferred Room Rendering** - Offscreen room groups are deferred in the All view
65+
- **Smart Re-renders** - Zustand-backed search state and stable device maps reduce dashboard churn
66+
- **No-Animation Mode** - Optional global animation disable for slower devices
6767
- **Tree-shakeable** - Only imports what's actually used
6868

6969
## 🚀 Installation
@@ -83,51 +83,43 @@ Navet (Swedish for "the hub") is a modern, responsive smart home dashboard built
8383

8484
2. **Install dependencies**
8585
```bash
86-
npm install
87-
# or
8886
pnpm install
8987
```
9088

9189
3. **Configure your smart home connection**
92-
93-
Update `/src/app/hooks/use-devices.ts` with your smart home system URL and token:
94-
```typescript
95-
const HA_URL = 'http://your-smart-home:8123';
96-
const HA_TOKEN = 'your-long-lived-access-token';
90+
91+
Create a local env file from the example and set your Home Assistant connection details:
92+
```bash
93+
cp .env.example .env
94+
```
95+
96+
Then set:
97+
```env
98+
NAVET_HASS_URL=http://your-home-assistant:8123
99+
NAVET_HASS_TOKEN=your-long-lived-access-token
97100
```
98101

99102
4. **Start development server**
100103
```bash
101-
npm run dev
104+
pnpm dev
102105
```
103106

104107
5. **Open in browser**
105-
106-
Navigate to `http://localhost:5173`
108+
109+
Navigate to `http://navet.homeassistant.local:5200`
107110

108111
## 📖 Usage
109112

110113
### First Time Setup
111114

112-
1. **Login** - Use default credentials (demo/demo123) or configure your own
113-
2. **Connect System** - Ensure your smart home instance is accessible
114-
3. **Customize Layout** - Enter Edit Mode to arrange cards
115+
1. **Provide Home Assistant credentials** via `.env`, Docker runtime config, or the in-app login screen
116+
2. **Connect System** - Ensure your Home Assistant instance is accessible
117+
3. **Customize Layout** - Enter Edit Mode to arrange rooms and cards
115118
4. **Choose Theme** - Select your preferred color scheme and theme mode
119+
5. **Optional for slower hardware** - Disable animations in Settings -> Performance
116120

117121
### Features Guide
118122

119-
#### Custom Widgets
120-
- Enter **Edit Mode** and click **Add Card**
121-
- Choose from 5 widget types:
122-
- **Calendar** - View upcoming events
123-
- **News Feed** - Latest headlines
124-
- **Weather** - Current conditions and forecast
125-
- **Photo Frame** - Beautiful photo carousel
126-
- **Quick Note** - Editable sticky notes
127-
- Select size (small/medium/large)
128-
- **Delete widgets** by clicking the X button in edit mode
129-
- Widgets are saved and persist across sessions
130-
131123
#### Edit Mode
132124
- Click the **Edit** button in the header
133125
- **Add widgets** with the Add Card button
@@ -170,9 +162,9 @@ Navet (Swedish for "the hub") is a modern, responsive smart home dashboard built
170162
- **Zustand** - State management with persistence
171163
- **Tailwind CSS v4** - Utility-first styling
172164
- **Radix UI** - Accessible component primitives
173-
- **React DnD** - Drag and drop functionality
165+
- **dnd-kit** - Drag and drop functionality
174166
- **Lucide React** - Icon library
175-
- **Motion** (Framer Motion) - Animations
167+
- **Vite** - Development server and build tooling
176168

177169
## 📄 License
178170

@@ -225,7 +217,7 @@ For technical documentation and developer guides, see [`/docs/README.md`](docs/R
225217

226218
## 🐛 Bug Reports & Feature Requests
227219

228-
Found a bug or have an idea? [Open an issue](https://github.com/awesomestvi/home-assistant-dashboard/issues)!
220+
Found a bug or have an idea? [Open an issue](https://github.com/awesomestvi/navet/issues)!
229221

230222
## 📸 Screenshots
231223

docs/DOCKER_HOME_ASSISTANT_ADDON.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,21 @@ If those are unset, Navet falls back to manual setup in the UI.
4949
### Local Run Example
5050

5151
```bash
52-
docker compose up --build
52+
docker compose pull
53+
docker compose up -d
5354
```
5455

5556
Then open `http://localhost:8080`.
5657

5758
### Release Pipeline
5859

59-
The GitHub Actions publish workflow is manual for private-repo use. It publishes a multi-arch image to GitHub Container Registry:
60+
The GitHub Actions publish workflow publishes a multi-arch image to GitHub Container Registry on pushes to `main`, and it can also be run manually:
6061

61-
- `ghcr.io/<owner>/navet:<version>`
62+
- `ghcr.io/<owner>/navet:main`
63+
- `ghcr.io/<owner>/navet:sha-<commit>`
6264
- `ghcr.io/<owner>/navet:latest`
6365

64-
Run it from the GitHub Actions UI and provide a version string such as `0.1.0-private.1`.
66+
Manual dispatch can still publish an additional version tag such as `0.1.0-private.1`.
6567

6668
### Private GHCR Deployment
6769

@@ -128,10 +130,9 @@ For private Home Assistant development:
128130

129131
For standalone Docker users:
130132

131-
1. Open the `Publish` workflow in GitHub Actions
132-
2. Enter a version like `0.1.0-private.1`
133-
3. Run the workflow
134-
4. Pull the updated image from GHCR on your deployment host
133+
1. Push changes to `main` or run the `Publish` workflow manually
134+
2. Pull the updated image from GHCR on your deployment host
135+
3. Restart the container with `docker compose up -d`
135136

136137
## Runtime Config Resolution
137138

@@ -141,6 +142,16 @@ The app resolves Home Assistant defaults in this order:
141142
2. Runtime `window.__NAVET_CONFIG__` from `/config.js`
142143
3. Build-time `import.meta.env.NAVET_HASS_URL` and `import.meta.env.NAVET_HASS_TOKEN`
143144

145+
## Current Performance Work
146+
147+
The current dashboard build includes a few runtime-focused optimizations:
148+
149+
- Lazy-loaded settings, add-card dialog, widgets, and media dialog
150+
- Deferred rendering for offscreen room groups in the All view
151+
- Zustand-backed search result state to reduce context fan-out
152+
- Stable device-map reuse to avoid rerendering unchanged cards
153+
- Optional no-animation mode for slower devices such as Raspberry Pi deployments
154+
144155
## Remaining Limitation
145156

146157
The current add-on still expects explicit Home Assistant connection details. It does not yet use Supervisor-native authentication or automatically derive a user-scoped Home Assistant session.
@@ -159,7 +170,7 @@ If you are the only consumer right now:
159170

160171
1. Keep the repo private
161172
2. Use local add-on installs for Home Assistant testing
162-
3. Use the manual GHCR publish workflow for remote Docker deployments
173+
3. Let pushes to `main` publish the GHCR image for remote Docker deployments
163174
4. Move to tag-based or public releases only when you are ready to distribute the project
164175

165176
## Notes Specific to This Repo
@@ -168,6 +179,7 @@ If you are the only consumer right now:
168179
- That only affects development and does not block Docker packaging
169180
- `index.html` now loads `/config.js` before the app bootstraps
170181
- Home Assistant credentials are still persisted in browser storage after login
182+
- Development builds log slow dashboard renders with `[Navet][RenderProfiler]`
171183

172184
## If You Want Full Add-on UX
173185

docs/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ Complete documentation for the Navet smart home dashboard.
4141
### Project History
4242
- **[Change History](archive/CHANGES.md)** - Major changes and migrations
4343
- Rebranding to Navet
44-
- Preact migration
45-
- Zustand migration
46-
- DnD kit migration
44+
- State management migrations
45+
- Drag-and-drop migration
4746
- Optimization history
4847
- **[Organization Summary](archive/CLEANUP_SUMMARY.md)** - Documentation cleanup details
4948

@@ -72,6 +71,9 @@ Complete documentation for the Navet smart home dashboard.
7271
**Run Navet in Docker or as a Home Assistant add-on:**
7372
→ See [Docker and Home Assistant Add-on](DOCKER_HOME_ASSISTANT_ADDON.md)
7473

74+
**Understand recent performance work:**
75+
→ See [Docker and Home Assistant Add-on -> Current Performance Work](DOCKER_HOME_ASSISTANT_ADDON.md#current-performance-work)
76+
7577
## 📖 Documentation Organization
7678

7779
This documentation is organized into:
@@ -120,5 +122,5 @@ docs/
120122

121123
---
122124

123-
**Last Updated:** March 6, 2026
125+
**Last Updated:** March 7, 2026
124126
**Documentation Status:** ✅ Complete and organized

src/app/App.tsx

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
useSensors,
99
} from '@dnd-kit/core';
1010
import { Lightbulb } from 'lucide-react';
11-
import { useCallback, useEffect, useMemo, useState } from 'react';
11+
import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react';
1212
import { toast } from 'sonner';
1313
import { RoomNav } from './components/layout/room-nav';
1414
import {
@@ -19,6 +19,7 @@ import {
1919
} from './components/layout/sections';
2020
import { EmptyState } from './components/shared/empty-state';
2121
import { LoadingSpinner } from './components/shared/loading-spinner';
22+
import { RenderProfiler } from './components/shared/render-profiler';
2223
import { Toaster } from './components/ui/sonner';
2324
import { AuthProvider, useAuth } from './contexts/auth-context';
2425
import { ConfigProvider, useConfig } from './contexts/config-context';
@@ -31,10 +32,9 @@ import { SearchProvider } from './contexts/search-context';
3132
import { ThemeProvider } from './contexts/theme-context';
3233
import { LoginPage } from './features/auth/login-page';
3334
import { AllViewGrid } from './features/dashboard/all-view-grid';
34-
import { AddCardDialog, type CardType } from './features/dashboard/components/add-card-dialog';
35+
import type { CardType } from './features/dashboard/components/add-card-dialog';
3536
import { DashboardLayout } from './features/dashboard/dashboard-layout';
3637
import { DeviceGrid } from './features/dashboard/device-grid';
37-
import { SettingsSection } from './features/settings/components/settings-section';
3838
import {
3939
useCardOrdering,
4040
useCardState,
@@ -47,6 +47,16 @@ import { useCustomCards } from './hooks/use-custom-cards';
4747
import { useDevices, useRooms } from './hooks/use-devices';
4848
import { useSettingsStore } from './stores';
4949

50+
const AddCardDialog = lazy(async () => {
51+
const module = await import('./features/dashboard/components/add-card-dialog');
52+
return { default: module.AddCardDialog };
53+
});
54+
55+
const SettingsSection = lazy(async () => {
56+
const module = await import('./features/settings/components/settings-section');
57+
return { default: module.SettingsSection };
58+
});
59+
5060
/**
5161
* Dashboard Component
5262
* The main dashboard view after authentication
@@ -230,7 +240,9 @@ function Dashboard() {
230240
<DashboardLayout>
231241
{lightDeviceMap.size > 0 ? (
232242
<EditModeProvider value={editModeContextValue}>
233-
<AllViewGrid deviceMap={lightDeviceMap} rooms={lightRooms} cardOrders={cardOrders} />
243+
<RenderProfiler id="LightsSection">
244+
<AllViewGrid deviceMap={lightDeviceMap} rooms={lightRooms} cardOrders={cardOrders} />
245+
</RenderProfiler>
234246
</EditModeProvider>
235247
) : (
236248
<EmptyState
@@ -254,7 +266,11 @@ function Dashboard() {
254266
if (activeSection === 'settings') {
255267
return (
256268
<DashboardLayout>
257-
<SettingsSection />
269+
<Suspense fallback={<LoadingSpinner message="Loading settings..." />}>
270+
<RenderProfiler id="SettingsSection">
271+
<SettingsSection />
272+
</RenderProfiler>
273+
</Suspense>
258274
</DashboardLayout>
259275
);
260276
}
@@ -280,30 +296,38 @@ function Dashboard() {
280296
/>
281297

282298
{activeRoom === 'All' ? (
283-
<AllViewGrid
284-
deviceMap={deviceMap}
285-
rooms={roomOrder}
286-
cardOrders={cardOrders}
287-
customCards={customCards}
288-
onDeleteCard={handleDeleteCard}
289-
onUpdateCard={handleUpdateCard}
290-
/>
299+
<RenderProfiler id="AllViewGrid">
300+
<AllViewGrid
301+
deviceMap={deviceMap}
302+
rooms={roomOrder}
303+
cardOrders={cardOrders}
304+
customCards={customCards}
305+
onDeleteCard={handleDeleteCard}
306+
onUpdateCard={handleUpdateCard}
307+
/>
308+
</RenderProfiler>
291309
) : (
292-
<DeviceGrid
293-
orderedCardIds={orderedCardIds}
294-
deviceMap={deviceMap}
295-
customCards={customCards}
296-
onDeleteCard={handleDeleteCard}
297-
onUpdateCard={handleUpdateCard}
298-
/>
310+
<RenderProfiler id={`DeviceGrid:${activeRoom}`}>
311+
<DeviceGrid
312+
orderedCardIds={orderedCardIds}
313+
deviceMap={deviceMap}
314+
customCards={customCards}
315+
onDeleteCard={handleDeleteCard}
316+
onUpdateCard={handleUpdateCard}
317+
/>
318+
</RenderProfiler>
299319
)}
300320

301-
<AddCardDialog
302-
open={showAddCardDialog}
303-
onClose={() => setShowAddCardDialog(false)}
304-
onAddCard={handleAddCard}
305-
currentRoom={activeRoom}
306-
/>
321+
{showAddCardDialog && (
322+
<Suspense fallback={null}>
323+
<AddCardDialog
324+
open={showAddCardDialog}
325+
onClose={() => setShowAddCardDialog(false)}
326+
onAddCard={handleAddCard}
327+
currentRoom={activeRoom}
328+
/>
329+
</Suspense>
330+
)}
307331
</DashboardLayout>
308332
</DndContext>
309333
</EditModeProvider>

src/app/components/figma/ImageWithFallback.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,26 @@ export const ImageWithFallback = memo(function ImageWithFallback(
2121
style={style}
2222
>
2323
<div className="flex items-center justify-center w-full h-full">
24-
<img src={ERROR_IMG_SRC} alt="Failed to load" {...rest} data-original-url={src} />
24+
<img
25+
src={ERROR_IMG_SRC}
26+
alt="Failed to load"
27+
loading="lazy"
28+
decoding="async"
29+
{...rest}
30+
data-original-url={src}
31+
/>
2532
</div>
2633
</div>
2734
) : (
28-
<img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
35+
<img
36+
src={src}
37+
alt={alt}
38+
loading={rest.loading ?? 'lazy'}
39+
decoding={rest.decoding ?? 'async'}
40+
className={className}
41+
style={style}
42+
{...rest}
43+
onError={handleError}
44+
/>
2945
);
3046
});

0 commit comments

Comments
 (0)