You're running a legacy codebase, supporting browser from 2013 and you want to avoid that your users are unable to use your service with your next release or you want to enrich your user experience with a new feature, but not writing more legacy code and not ship more polyfills.

Here is my approach how to write code in 2021, even on existing codebases.

the unused code dilemma

By supporting old browsers you usually add polyfills to your code - filling missing javascript functionality with (more) code.

That more code leads into a real problem because javascript is treated specially in your browser. The trinity of javascript processing within your browser (loading, parsing, executing) is renderblocking - means your users browser will not do anything while he is processing javascript.
By using async or defer in your script tag the downloading javascript is not renderblocking anymore, but parsing and executing will freeze your users browser.

Adding polyfills to enable more browsers to be able to work with your page, the bundle size increases and the javascript processing time too.
Modern browsers do not need many polyfills and on executing of you bundle, they skip the polyfills they do not need, resulting in unused code. Another flavour of unused code are components which are bundled but not used on that page for example.

If the browser comes at the end to the point where it sees 80% of all that javascript is not needed to render/show this page, you can imagine that 80% of the code parsing and executing time could be saved.

stackoverflow.com startpage code coverage on chrome

The screenshot above is from the stackoverflow homepage and chromes code coverage tool. A high unused code number does not always points out a problem it is more a sign that a deep dive into that bundle might be valueable.

whose impacted the most?

Assuming you ship polyfills to support IE11 plattform, this code will run on a desktop machine, a desktop cpu & ram environment. Maybe the hardware is a little bit outdated and this audience might be used to "wait a little" for webpages to respond.
But chances are real that this audience may able to upgrade to a modern browser, but as long as the page is working on the old browser, there is no need to meet a new browser.

When we look at the android market (phones and tablets) we notice that there exists devices from high- to low-end and most-sold-devices are not in the highend segment. By polyfilling your code to make it runable on 1% of your audience, you highly impact a bigger percentage of your audience, because these devices usually do have an up to date browser, but not much cpu power and javascript performance cruicial depends on cpu power.

A good read on that topic is "The Mobile Performance Inequality Gap, 2021".

de-bundling vs caching vs unused code

With http/2 a GET-request is not that expensive anymore, a good idea might be de-bundling your assets (css and js) and deliver only page-needed assets.

On the other hand you will feel on every subsequent page that more assets need to be downloaded, which makes a next page feel a little bit slower.

So you might think it is a good practice to bundle all js and css into one big file each, ship it on first impression, the browser caches it and every subsequent page feels fast. But some visitors will not visit any second page, because the first impression with all that unused but downloaded, parsed and executed code they were scared away.

steps of gracefully upgrading your codebase

step 0: keep focus on developer experience

I assume some people can create highly optimized build pipeline to split everything into its pieces and polyfilling here and there where it is needed, maybe on serverlevel (node,nginx,apache,etc.) regarding every requests' user agent.

Coming from a polyfilled one-for-all-bundle you should go one step by another and always take all team members with you. By setting up a pipelined tooling like this, local developing may becomes unpleasent.
If you keep local developing pleasant it will directly improve your codebase quality because every developer understands the whole buildingprocess and feels more home with it. If a build pipeline does code optimization, some might get lazy during developing because "it is handled in build step X".

step 1: tell your user as soon as possible about it

As soon as you make the decision to write code that might not run in your current audience browser, add an info icon that tells your users, that upcoming versions might not work in their browser and give a hint to up to date browsers.
At least provide the 4 major browsers in that list (firefox, edge, chrome, safari) and when you know about specific browsers your visitors like to use, add this to.

For example: the next big version will use es modules - tell your user about it, even if your roadmap tells q3 2022 for that feature.
You want to build a new feature with a modern js framework, tell users which might not be able to use this feature with their current browser.

step 2: use progressive enhancement technologies

Don't read a user agent and decide if your code will work in future or not. Make yourself a list of features you are using and build a custom shaped feature detection script to match every users browser to meet your requirements. In the early 2010's we usually put a modernizr copy into the <head> and used nearly nothing from it. So check only for features you will use and keep that list updated.

And, as I wrote in step 1, tell your visitors as soon as possible that they might run into trouble with their current setup.

For example: the next version or a feature will user CSS custom properties. Add an feature detection script and let your visitors know about it.

step 3: include only what is needed

This advice aims to 3rd party plugins or frameworks. Always keep an eye on the size of anything 3rd party you use in your project. I use bundlephobia with every new package I include into a project. For example if you need a markdown processor in your frontend you can choose between snarkdown or markdown if both fullfill your requirements.
By using a ui-framework, for example bootstrap, you should only import that css and js code, which is used in your project, and do not import all of it.

step 4: after the launch

You released that feature or the next version and finally some browsers are not able to run your project anymore.

Now it is important to keep that feature detection and user hinting advice in your project. Add a little vanilla js/css/html code to your project, which works in all browsers and telling your visitors to upgrade their browser, if an error occurs.

As soon as you put out the unsupported browsers of your quality-assurance line you do not see how your project is rendered on this browsers and you might be suprised what an IE11 will render to the user. A server side rendered js framework page will create some output, a client side rendered js framework page will lead to a blank? page but maybe there is a static header and footer which is displayed.

So make sure to catch every user whose browser got trouble consuming your site and provide information how to visit your page probably.

step 5: track this data

In every step before, you must track users whose browser is not able to run your page probably. You might think that IE11 is your problem, but by collecting every user running into an non working page, you get out of this "Browser XY is blocking us to move on" and you start to address your codebase issues.
With that data, you can make edjucated guess' if you need to include one more polyfill to cover more visitors.

summary

When upgrading your codebase from legacy to legacy@next ;-) you should inform your users as soon as possible when there is a plattform support change.

Do not use any kind of user agent sniffing to adress not supported plattforms, use accurate feature detection to find all unsupported browsers.

Keep an eye on every 3rd party codepieces and check if there is a lightweight alternative for that usecase.

Track all this data and adjust your codebase if you find a user setup that did not meet your requirements and you were not aware of.

Always keep in mind how javascript is processed in the browser (download, parse, execute) and that android offers a manyfold environment regarding cpu performance. Every javascript byte you add to enable a 1% browser will have a low to high impact on the other 99%.

You might be interested on that read: "Remove dust from a legacy project" here in this blog.

Article Image from Matt Seymour via unsplash and ghost