Iva Kop I am a self-taught software developer passionate about frontend development and architecture.

How to detect dead code in a frontend project

5 min read 1513

Tool Logos Over Code Background

Having a large amount of dead code in a project can be detrimental to your app for many reasons. Dead code makes the codebase substantially harder to maintain at scale. It also has the potential to create confusion within the development team as to which pieces of code are relevant and actively worked on and which ones can be safely ignored.

The best way to avoid these pitfalls is to ensure we have the appropriate tooling in place to allow us to detect dead code both reliably and automatically.

In this article, we’ll cover three ways to detect dead code in your frontend project, plus an extra bonus section:

Using ESLint to detect and remove dead code

ESLint is perhaps the most widely used JavaScript linter, with nearly 25 million weekly downloads on npm. It is — or at least should be — an integral part of every JavaScript project. Among many other useful things, ESLint allows us to detect unused variables in our files with its aptly named no-unused-vars rule.

This rule protects us from introducing dead code in two ways. First, it will tell us if we have declared a variable that is not used elsewhere in the file:

// Variable is defined but never used
let x;

// Write-only variables are not considered as used.
let y = 10;
y = 5;

// A read for a modification of itself is not considered as used.
let z = 0;
z = z + 1;

Second, it will tell us if there are unused arguments in our functions:

// By default, unused arguments cause warnings.
(function(foo) {
    return 5;
})();

// Unused recursive functions also cause warnings.
function fact(n) {
    if (n < 2) return 1;
    return n * fact(n - 1);
}

// When a function definition destructures an array, unused entries from the array also cause warnings.
function getY([x, y]) {
    return y;
}

To enable the rule, you can simply add it to the rules object in your ESLint configuration file. The "extends": "eslint:recommended" property in the same file also enables the rule. Find more information on ESLint rules and how to configure them here. Most of the behavior described above is configurable and can be tweaked to fit specific project needs.

The no-unused-vars ESLint rule is an excellent tool for dead code detection. Its usage is strongly recommended both for local development and as part of a continuous integration pipeline.

But on its own, this rule is not enough to ensure we detect all dead code in our project. How come?

The vast majority of modern frontend projects use ECMAScript modules to organize and reuse code via imports and exports. This means that even if a variable or function is not used within the file, as long as it is exported, it is no longer considered unused.

We made a custom demo for .
No really. Click here to check it out.

// Variable is defined but never used
const x = 10

// The no-unused-vars rule is not broken
export const y = 20

We seem to have hit the limit of this ESLint rule. So, how can we know all exported code is imported and used elsewhere in our project and, therefore, not dead?

We can continue using our linter and take advantage of a plugin called eslint-plugin-import and, more specifically, its rule no-unused-modules, which allows us to detect both modules without exports, as well as exports that are not imported in other modules.

To set it up, simply install it:

npm install eslint-plugin-import --save-dev

Then, add the plugin and the rule to the ESLint configuration file:

"plugins: {
  ...otherPlugins,
  'import',
},
"rules: {
  ...otherRules,
  "import/no-unused-modules": [1, {"unusedExports": true}]
}

While the plugin is excellent and actively supported, there still might be good reasons to use another approach. For example, what if our project is not using ESLint at all? Perhaps we want to detect unused imports and exports only in our CI pipeline to make our linter lighter for speedier local development, and we don’t want to support different configurations for both environments. Or maybe we ran into some of the open issues with this setup.

Whatever the case may be, there is an alternative solution: webpack

Using webpack for dead code detection

Webpack is a module bundler that is widely used in modern web apps. Its main purpose is to bundle JavaScript files for usage in a browser. Essentially, webpack is used to create a dependency graph of your application and combine every module of your project into a bundle. This makes the tool perfectly positioned to detect unused imports and exports, i.e., dead code.

While webpack will automatically attempt to remove unused code in the bundle it produces (learn more about tree-shaking here), there is a handy plugin to help us detect unused files and exports in our code in the process of writing it — webpack-deadcode-plugin.

To add the plugin to your project, first install it:

npm install webpack-deadcode-plugin --save-dev

Now add it to your webpack configuration file, like so:

const DeadCodePlugin = require('webpack-deadcode-plugin');

const webpackConfig = {
  ...
  optimization: {
    usedExports: true,
  },
  plugins: [
    new DeadCodePlugin({
      patterns: [
        'src/**/*.(js|jsx|css)',
      ],
      exclude: [
        '**/*.(stories|spec).(js|jsx)',
      ],
    })
  ]
}

The plugin will then automatically report unused files and unused exports into your terminal. It’s a useful tool that can enhance the development process and ensure we are not introducing dead code in a project.

But it has its limitations.

First, the tool’s output in the terminal can get lost or be difficult to parse, depending on other outputs shown in the same place. This can make it inconvenient to use.

Second, it might be slightly more difficult to include it in your CI pipeline. The reason is that, according to the documentation:

The plugin will report unused files and unused exports into your terminal but those are not part of your webpack build process, therefore, it will not fail your build

Finally, and perhaps most importantly, the plugin’s output might be incorrect when using it in a TypeScript project.

Lucky for us, TypeScript itself can be used for dead code detection!

Using TypeScript to detect dead code

Using TypeScript in our project has a number of advantages. One of them is that it provides us with an easy way to detect dead code.

First, we can configure TypeScript in a way that doesn’t allow for unused local variables and function parameters. This is similar to the no-unused-vars ESLint rule above. To enforce these rules via TypeScript, we can add them to our tsconfig file:

{
  "compilerOptions": {
    ...otherOptions,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
  }
}

But, even with these checks in place, we still face the issue of unused exports. Let’s use ts-prune for the job. It is easy to use and requires very little (if any) configuration.

To use it, we need to install it:

npm install ts-prune --save-dev

Then, add a script for it to our package.json file:

{
  "scripts": {
    "find-deadcode": "ts-prune"
  }
}

Now, every time we run npm run find-deadcode in our project, we will have a list of all unused exports.

Note that the script above will detect unused exports, even if they are used internally within the module. To opt out of this behavior, modify your script to exclude these:

"find-deadcode": "ts-prune | grep -v '(used in module)'"

Finally, if you want to use ts-prune in CI, you will need to change the exit code so that an error occurs when there are unused exports. Here’s the final modification:

"find-deadcode": "ts-prune | (! grep -v 'used in module')"

Of the presented options, TypeScript can arguably enforce dead code detection in the strictest way. So, if you are working on a TypeScript project, using Typescript-specific tooling — including the compile options in tsconfig , @typescript-eslint/eslint-plugin (to combine with ESLint) and ts-prune — is often going to be the optimal approach.

Bonus: Detect unused dependencies with depcheck

While we’re on the subject of dead code detection, let’s briefly discuss how to ensure we don’t have unused dependencies in our project. Let’s use depcheck, a tool for analyzing the dependencies in a project. It can tell us:

  • How each dependency is used
  • Which dependencies are useless
  • Which dependencies are missing from package.json

Install it with:

npm install -g depcheck

Next, run the check.

npx depcheck

Depcheck uses a special component that allows us to recognize dependencies used outside of the regular import/export flow. These include dependencies used in configuration files, npm commands, scripts, and more.

Conclusion

In this article, we explored different approaches to detect dead code in your frontend project. These approaches can be used both interchangeably and in combination with one another. As is often the case, choosing our ideal setup depends heavily on the particular use case.

It’s also important to note that these are not the only existing dead code detection tools. They were selected based on the prevalence and popularity of the underlying tools (ESLint, webpack, and TypeScript). But, depending on the particularities of the project, the optimal solution might not be on this list.

If you found this article useful, visit my blog and follow me on Twitter for more tech content.

Happy coding! ✨

Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

Iva Kop I am a self-taught software developer passionate about frontend development and architecture.

3 Replies to “How to detect dead code in a frontend project”

  1. Great article! 👍

    Regarding this part 👇

    > The plugin will report unused files and unused exports into your terminal but those are not part of your webpack build process, therefore, it will not fail your build

    According to their docs, there is a way `failOnHint` to fail the build if the `webpack-deadcode-plugin` finds something.

    > options.failOnHint (default: false)
    > Deadcode does not interrupt the compilation by default. If you want to cancel the compilation, set it true, it throws a fatal error and stops the compilation.

    https://github.com/MQuy/webpack-deadcode-plugin#optionsfailonhint-default-false

    1. Great point, indeed 👍 Thanks for pointing it out – definitely good to keep this option in mind if you want to use `webpack-deadcode-plugin` in CI!

  2. One small remark 🙂

    You don’t need to install depcheck before `npx depcheck`. There are just two options: do `npm i -g depcheck` or `npx depcheck`

Leave a Reply