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:
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
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 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.
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:
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.
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! ✨
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
3 Replies to "How to detect dead code in a frontend project"
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
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!
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`