How to detect dead code in a frontend project

How to detect dead code in a frontend project

Iva Kop's photo
Iva Kop
·May 13, 2022·

7 min read

Subscribe to my newsletter and never miss my upcoming articles

Listen to this article

Table of contents

  • Using ESLint to detect and remove dead code
  • Using webpack for dead code detection
  • Using TypeScript to detect dead code
  • Bonus: detect unused dependencies with depcheck
  • Conclusion

Having large amounts 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.

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.

// 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

There is a possibility to opt out of this behaviour, though, by enabling the failOnHint setting.

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](https://www.npmjs.com/package/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, continue reading my blog and follow me on Twitter for more tech content.

Happy coding! ✨

Did you find this article valuable?

Support Iva Kop by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this