nuxt-icon can cause serious memory leaks.
Describe the bug nuxt-icon can cause serious memory leaks.
To Reproduce
Build and run the Nuxt 3 project, then perform load testing:
-
Build:
pnpm build -
Run server:
node .output/server/index.mjs -
Load test using autocannon:
npx autocannon -c 40 -d 30 http://localhost:3000
1. With index.vue reduced to only a single <div>
Memory remains stable.
| Test | Memory Usage |
|---|---|
| Initial | 25.1M |
| After 1st load test | 37.0M |
| After 2nd | 44.6M |
| After 3rd | 39.7M |
| After 4th | 40.3M |
| After 5th | 39.9M |
| After 6th | 40.2M |
| After 5 minutes | 40.2M |
2. Restore part of the code + enable nuxt-icons
Memory usage increases dramatically and leads to OOM after several rounds.
| Test | Memory Usage |
|---|---|
| Initial | 20.2M |
| After 1st load test | 1378.4M |
| After 2nd | 2643.6M |
| After 3rd | 3678.1M |
At the 4th test, the following error appears in the console:
<--- Last few GCs --->
[30424:0000014AD5201000] 272236 ms: Mark-Compact 4014.8 (4129.0) -> 3994.8 (4124.8) MB, pooled: 0 MB, 853.34 / 0.02 ms (average mu = 0.214, current mu = 0.059) allocation failure; scavenge might not succeed
[30424:0000014AD5201000] 273138 ms: Mark-Compact 4013.8 (4128.0) -> 3993.9 (4124.0) MB, pooled: 0 MB, 869.00 / 0.01 ms (average mu = 0.131, current mu = 0.036) allocation failure; scavenge might not succeed
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
3. Remove nuxt-icons
Memory becomes stable again.
| Test | Memory Usage |
|---|---|
| Initial | 20.9M |
| After 1st load test | 38.6M |
| After 2nd | 39.9M |
| After 3rd | 40.6M |
| After 4th | 41.5M |
| After 5th | 41.5M |
| After 6th | 41.9M |
| After 5 minutes | 41.9M |
package.json
{ "name": "nuxt-app", "private": true, "type": "module", "scripts": { "build": "nuxt build --dotenv .env.prod", "dev": "nuxt dev --host --dotenv .env.dev" }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", "@element-plus/nuxt": "^1.0.8", "@floating-ui/dom": "1.6.0", "@internationalized/date": "^3.8.1", "@nuxtjs/i18n": "^8.5.6", "@tailwindcss/vite": "^4.1.5", "@tanstack/vue-table": "^8.21.3", "@tato30/vue-pdf": "^1.11.4", "@types/file-saver": "^2.0.7", "@unhead/vue": "1.10.4", "@vee-validate/zod": "^4.15.0", "@vueuse/core": "^13.4.0", "@zadigetvoltaire/nuxt-gtm": "^0.0.13", "alfaaz": "^1.1.0", "class-variance-authority": "^0.7.1", "dayjs": "^1.11.13", "dayjs-nuxt": "2.1.11", "echarts": "^5.6.0", "element-plus": "^2.6.3", "file-saver": "^2.0.5", "js-md5": "^0.7.3", "jspdf": "2.5.1", "katex": "^0.16.22", "lodash": "^4.17.21", "lucide-vue-next": "^0.508.0", "mupdf-webviewer": "^0.9.1", "nprogress": "^0.2.0", "nuxt": "3.18.1", "pinia": "^2.2.1", "reka-ui": "2.2.1", "rollup-plugin-visualizer": "^5.14.0", "shadcn-nuxt": "^2.1.0", "spark-md5": "^3.0.2", "tailwind-merge": "^3.2.0", "tailwindcss": "^4.1.10", "tippy.js": "^6.3.7", "tw-animate-css": "^1.2.9", "vee-validate": "^4.15.0", "vite-plugin-compression": "^0.5.1", "vite-plugin-imagemin": "^0.6.1", "vue": "^3.5.16", "vue-router": "^4.5.1", "vue-sonner": "^2.0.0", "vue3-google-login": "2.0.29", "y-protocols": "^1.0.6", "yjs": "^13.6.27" }, "devDependencies": { "@iconify/utils": "^3.1.0", "@nuxtjs/tailwindcss": "^6.12.1", "@pinia-plugin-persistedstate/nuxt": "^1.2.1", "@pinia/nuxt": "^0.5.3", "@vitejs/plugin-vue-jsx": "^4.1.2", "sass": "^1.77.8", "typescript": "^5.9.2", "vite-svg-loader": "^5.1.0", "xlsx": "^0.16.1" }, "pnpm": { "overrides": { "jiti": "^2.5.1", "vite": "^7.0.6", "prosemirror-model": "1.19.0", "vue": "3.5.16", "@vue/compiler-sfc": "3.5.16", } }, "resolutions": { "vue": "3.5.16", "@vue/compiler-sfc": "3.5.16" } }
Troubleshooting Attempts
I tried multiple dependency and configuration adjustments. The following steps showed noticeable improvements and prevented the memory leak crash:
-
Downgraded / adjusted several package versions (effective):
nuxt: 3.12.4 vue: 3.5.13 @nuxtjs/i18n: ^8.3.0 nuxt-icons: ^3.2.1 reka-ui: 2.0.0 shadcn-nuxt: 2.0.0 @pinia-plugin-persistedstate/nuxt: ^1.2.1 -
Removed the
vue-sonnerpackage or commented out related code (This also reduced memory growth.) -
Updated
nuxt.config.jsaccording to the version changes above (Some modules required different configuration formats.) -
Performed load testing using:
npx autocannon -c 40 -d 60 \ -H "Authorization: Basic cGlwaWFkczoxMjMzMjFzdW4=" \ https://XXXX.com
Results After Adjustments
The memory was successfully released during and after stress testing, and the server no longer crashed due to out-of-memory errors.
I also encountered a memory leak issue with nuxt icons. The version is "nuxt icons": "^ 3.2.1". How did you solve it
I also encountered a memory leak issue with nuxt icons. The version is "nuxt icons": "^ 3.2.1". How did you solve it
I removed the nuxt-icons dependency and related configurations; I extracted the author's source code and repackaged it into a NuxtIcon.vue component.
The NuxtIcon.vue code is as follows:
<template> <span class="nuxt-icon" :class="{ 'nuxt-icon--fill': !filled, 'nuxt-icon--stroke': hasStroke && !filled }" v-html="icon" /> </template>
`
const props = withDefaults(defineProps<{ name: string filled?: boolean }>(), { filled: false })
const iconsImport = import.meta.glob('/assets/icons/**/*.svg', { eager: false, query: '?raw', import: 'default' })
const icon = ref
async function getIcon() {
try {
const iconPath = /assets/icons/${props.name}.svg
if (!iconsImport[iconPath]) {
console.error([nuxt-icons] Icon '${props.name}' doesn't exist in 'assets/icons')
icon.value = ''
return
}
const rawIcon = await iconsImporticonPath as string
if (rawIcon.includes('stroke')) {
hasStroke = true
}
icon.value = rawIcon
} catch (error) {
console.error([nuxt-icons] Failed to load icon '${props.name}':, error)
icon.value = ''
}
}
await getIcon()
watchEffect(getIcon) `
`
.nuxt-icon svg { width: 1em; height: 1em; margin-bottom: 0.125em; vertical-align: middle; }
.nuxt-icon.nuxt-icon--fill, .nuxt-icon.nuxt-icon--fill * { fill: currentColor !important; }
.nuxt-icon.nuxt-icon--stroke, .nuxt-icon.nuxt-icon--stroke * { stroke: currentColor !important; } `