The container-only harness was measuring the wrong thing. Real usage — several tabs, several boards, clicking into widgets — reaches ~880 MB server and a 650 MB *browser* heap, and the browser was never measured at all. Profiling build: `docker build --build-arg HOMARR_PROFILING=true . -t homarr:performance`. React's production bundle strips the hooks DevTools needs, hence "Profiling not supported". Note reactProductionProfiling in next.config is read only by the webpack path; under Turbopack it comes from `next build --profile`, and turbo strips any env var not declared in the task's `env`, so all three had to line up before the flag actually took effect — verified by markers present only in React's profiling bundle, and static chunks going 20 -> 84 MB with source maps. inspect-container.mts attaches to an already-running container (SIGUSR1 opens Node's inspector — no rebuild, no restart), reports cgroup/per-process/V8 memory and can pull a heap snapshot out over CDP. The inspected context has no require and no dynamic import, so it reads globals and uses CDP domains instead. analyze-heapsnapshot / analyze-react-fibers / analyze-heap-retainers summarise a snapshot too large for DevTools to open. Harness now opens STRESS_BOARDS across STRESS_TABS_PER_BOARD tabs, clicks into widgets, and records browser heap, DOM nodes and listener count via CDP. Performance.getMetrics silently returns nothing unless the domain is enabled, which is why the first run reported a 0 MiB client heap.
145 lines
5.1 KiB
TypeScript
145 lines
5.1 KiB
TypeScript
// Importing env files here to validate on build
|
|
import path from "node:path";
|
|
|
|
import "@homarr/auth/env";
|
|
import "@homarr/core/infrastructure/db/env";
|
|
import "@homarr/common/env";
|
|
import "@homarr/core/infrastructure/logs/env";
|
|
import "@homarr/docker/env";
|
|
|
|
import type { NextConfig } from "next";
|
|
import createNextIntlPlugin from "next-intl/plugin";
|
|
|
|
// Package path does not work... so we need to use relative path
|
|
const withNextIntl = createNextIntlPlugin({
|
|
requestConfig: "../../packages/translation/src/request.ts",
|
|
});
|
|
|
|
/**
|
|
* Profiling build. React's production bundle strips the hooks React DevTools needs,
|
|
* which is why the Profiler tab reports "Profiling not supported"; the profiling
|
|
* variant keeps them. Source maps are emitted alongside so heap snapshots and flame
|
|
* charts show real component names instead of minified ones like `y`.
|
|
*
|
|
* Build with:
|
|
* docker build --build-arg HOMARR_PROFILING=true . -t homarr:performance
|
|
*
|
|
* `reactProductionProfiling` in this config is only read by the webpack path — under
|
|
* Turbopack it comes from `next build --profile`, which apps/nextjs/package.json
|
|
* passes when HOMARR_PROFILING=true. Both are set so either bundler works.
|
|
*
|
|
* Off by default: the profiling variant is slower and source maps are large, so this
|
|
* must never be what ships to users.
|
|
*/
|
|
const isProfilingBuild = process.env.HOMARR_PROFILING === "true";
|
|
|
|
const nextConfig: NextConfig = {
|
|
reactProductionProfiling: isProfilingBuild,
|
|
productionBrowserSourceMaps: isProfilingBuild,
|
|
// Next previews otherwise create agent instruction files in the application
|
|
// directory during development.
|
|
agentRules: false,
|
|
env: {
|
|
HOMARR_VERSION: process.env.HOMARR_VERSION ?? "unknown",
|
|
},
|
|
output: "standalone",
|
|
reactStrictMode: true,
|
|
// react compiler breaks mantine-react-table, so disabled for now
|
|
//reactCompiler: true,
|
|
/** We already do typechecking as separate tasks in CI */
|
|
typescript: { ignoreBuildErrors: true },
|
|
/**
|
|
* dockerode is required in the external server packages because of https://github.com/homarr-labs/homarr/issues/612
|
|
*
|
|
* Everything else here is a server-only dependency that is cheaper left as a
|
|
* runtime require than inlined into the server bundle. Bundling pulls a package's
|
|
* whole transitive graph into a chunk that is loaded and compiled at boot even
|
|
* when the code path never runs — measurably so for the DB drivers: only
|
|
* better-sqlite3 is ever used, yet inlining mysql2 and pg cost ~21 MiB of
|
|
* resident memory at idle.
|
|
*/
|
|
serverExternalPackages: [
|
|
"dockerode",
|
|
"better-sqlite3",
|
|
"mysql2",
|
|
"pg",
|
|
"winston",
|
|
"drizzle-orm",
|
|
"ical.js",
|
|
"jszip",
|
|
"ldapts",
|
|
"node-unifi",
|
|
"@kubernetes/client-node",
|
|
"linkedom",
|
|
// Only reachable through /api/mcp/[transport].
|
|
"@modelcontextprotocol/sdk",
|
|
],
|
|
experimental: {
|
|
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
|
|
turbopackFileSystemCacheForBuild: true,
|
|
useTypeScriptCli: true,
|
|
},
|
|
turbopack: {
|
|
root: path.resolve(import.meta.dirname, "../.."),
|
|
// Development runs tasks and WebSocket as separate processes. These aliases
|
|
// keep their production-only instrumentation imports out of the dev graph.
|
|
resolveAlias:
|
|
process.env.NODE_ENV === "development"
|
|
? {
|
|
"@homarr/tasks": path.resolve(import.meta.dirname, "src/instrumentation-noop.ts"),
|
|
"@homarr/websocket": path.resolve(import.meta.dirname, "src/instrumentation-noop.ts"),
|
|
}
|
|
: undefined,
|
|
},
|
|
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
|
|
images: {
|
|
localPatterns: [
|
|
{
|
|
pathname: "/**",
|
|
search: "",
|
|
},
|
|
],
|
|
},
|
|
// skipcq: JS-0116
|
|
// eslint-disable-next-line @typescript-eslint/require-await,no-restricted-syntax
|
|
async headers() {
|
|
return [
|
|
{
|
|
source: "/sw.js",
|
|
headers: [
|
|
{ key: "Content-Type", value: "application/javascript; charset=utf-8" },
|
|
{ key: "Cache-Control", value: "no-cache, no-store, must-revalidate" },
|
|
{ key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self'" },
|
|
],
|
|
},
|
|
{
|
|
source: "/(.*)", // Apply CSP to all routes
|
|
headers: [
|
|
{
|
|
key: "Content-Security-Policy",
|
|
// worker-src / media-src with blob: is necessary for video.js, see https://github.com/homarr-labs/homarr/issues/3912 and https://stackoverflow.com/questions/65792855/problem-with-video-js-and-content-security-policy-csp
|
|
value: `
|
|
default-src 'self';
|
|
script-src * 'unsafe-inline' 'unsafe-eval';
|
|
worker-src * blob:;
|
|
base-uri 'self';
|
|
connect-src *;
|
|
style-src * 'unsafe-inline';
|
|
frame-ancestors *;
|
|
frame-src *;
|
|
form-action 'self';
|
|
img-src * data:;
|
|
font-src * data:;
|
|
media-src * data: blob:;
|
|
`
|
|
.replace(/\s{2,}/g, " ")
|
|
.trim(),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
},
|
|
};
|
|
|
|
export default withNextIntl(nextConfig);
|