Clean Up Unused Assets in public/ with a Node.js Script
DRAFT 01

Clean Up Unused Assets in public/ with a Node.js Script

Scan your public folder, find files nothing references, and delete them safely before deploy.

Context

Over time, web projects often accumulate unused files in the public/ directory—images, icons, test files, and more. These unused assets increase build size, slow down deployments, and clutter your repo.

To solve this, I created a Node.js script that scans the public/ folder, checks which assets are actually referenced in the project, and gives you the option to delete the unused ones.

How it works

  • Recursively collects all files in the public/ directory.
  • Scans your entire project (excluding folders like node_modules, .git, dist, etc.).
  • Supports common frontend file extensions: .js, .ts, .vue, .html, .css, and more.
  • Checks whether each public file is referenced anywhere in your codebase.
  • Prompts you before deleting any files.

When to use this

  • Before deployment to reduce bundle size
  • During codebase cleanups or refactoring
  • To maintain a tidy and optimized public/ folder

This script is especially useful for teams working on large frontend projects, or those who've gone through multiple redesigns where assets were left behind.

The script

Install readline-sync if you want interactive confirmation:

npm install readline-sync

Save the following as something like scripts/find-unused-public-assets.mjs and run it from your project root:

import { existsSync, readdirSync, readFileSync, unlinkSync } from "fs";import { join, extname, relative, sep } from "path";import { question } from "readline-sync";const PUBLIC_DIR = join(process.cwd(), "public");const IGNORE_DIRS = ["node_modules", ".git", "dist", "build"];const FILE_EXTENSIONS = [  ".html",  ".css",  ".js",  ".jsx",  ".ts",  ".tsx",  ".vue",  ".svelte",  ".php",  ".md",];function getFilesRecursively(dir, ignoreDirs = []) {  if (!existsSync(dir)) return [];  const files = readdirSync(dir, { withFileTypes: true });  let results = [];  for (const file of files) {    const fullPath = join(dir, file.name);    if (file.isDirectory()) {      if (!ignoreDirs.includes(file.name)) {        results = results.concat(getFilesRecursively(fullPath, ignoreDirs));      }    } else {      results.push(fullPath);    }  }  return results;}function findUnusedAssets() {  console.log("Scanning public directory...");  const publicFiles = getFilesRecursively(PUBLIC_DIR);  if (publicFiles.length === 0) {    console.log("No files found in public directory.");    return;  }  console.log("Scanning project files...");  const projectFiles = getFilesRecursively(process.cwd(), IGNORE_DIRS).filter((file) =>    FILE_EXTENSIONS.includes(extname(file).toLowerCase()),  );  const unusedFiles = [];  const checkedPatterns = new Set();  console.log("Checking for unused assets...");  for (const publicFile of publicFiles) {    const relativePath = relative(PUBLIC_DIR, publicFile);    const searchPattern = "/" + relativePath.split(sep).join("/");    if (checkedPatterns.has(searchPattern)) continue;    checkedPatterns.add(searchPattern);    let isUsed = false;    for (const projectFile of projectFiles) {      try {        const content = readFileSync(projectFile, "utf8");        if (content.includes(searchPattern)) {          isUsed = true;          break;        }      } catch (err) {        console.error(`Skipping ${projectFile}: ${err.message}`);      }    }    if (!isUsed) {      unusedFiles.push(publicFile);    }  }  if (unusedFiles.length === 0) {    console.log("No unused assets found.");    return;  }  console.log("\nFound unused files:");  unusedFiles.forEach((file) => console.log(`- ${file}`));  const confirm = question("\nDo you want to delete these files? (y/n) ");  if (confirm.toLowerCase() === "y") {    console.log("\nDeleting files...");    unusedFiles.forEach((file) => {      try {        unlinkSync(file);        console.log(`Deleted: ${file}`);      } catch (err) {        console.error(`Error deleting ${file}: ${err.message}`);      }    });    console.log("Deletion complete.");  } else {    console.log("Deletion cancelled.");  }}findUnusedAssets();

Run it:

node scripts/find-unused-public-assets.mjs

The script lists files under public/ that never appear in your source, then waits for y before deleting anything.

Conclusion

Keeping public/ lean is low effort with high payoff: smaller deploys, less noise in reviews, and fewer “is this file still used?” moments. Run the scan before a release or after a redesign and treat the prompt as the final safety check.