Ride down into JavaScript dependency hell
blog.appsignal.com
blog.appsignal.com
- Don't mention other languages, lest you reveal that most of them have similar dependency bloat problems.
- Talk about devDependencies and dependencies without considering how they might be entirely different, say… one kind gets bundled, the other doesn't.
- Always use npm, so you can highlight duplicates. Don't use yarn.
- Adopt a narrow definition of 'dependency hell' so you can focus on the thing that JavaScript does imperfectly (minimizing number of dependencies) and avoid talking about the things that it does well (handling conflicting deps, solving diamond deps, resolving dependency chains)
- Don't worry about explaining how any of the issues you're discussing have real-world implications. Have companies been failing because they run out of disk space because of node_modules?
- Take automated security audits extremely seriously. A backtracking bug in the argument parser for the web framework's local CLI tool? Of course it's not exploitable, when… it's a web app, and the CLI tool is something you run locally. But add it to the count as if it is!
Ignore the ridiculously small functionality scope that acceptably qualifies for a js package in a way that does not exist in any other ecosystem and ads a big multiplier in front of all the other risks involved.
I'm pretty cautious about third party dependancies, but some of those libraries' libraries are not, and it's really hard to see that coming.
If you expect to live with code for 5+ years (and I understand that's not everyone, but it's almost always me), this is a big problem.
The post contains a graph with the package counts for other languages [1]
1 https://d33wubrfki0l68.cloudfront.net/7481900a584f733e7e9db7...
For example: The npm package "is-even" depends on the "is-odd" package. (In case it's not clear, this is a real package, not a made-up example.) In most other languages, these would be bundled together into an Integer Utilties package, if they're not already part of the standard library. In NPM, they are separate packages.
I believe there is, or was, a technical reason for NPM to have very fine-grained imports. Perhaps something about not supporting partial imports? I can't exactly find it. But the point is: NPM having more packages does not translate to a larger and more vibrant ecosystem. In some cases it simply means accomplishing the same thing requires a larger dependency graph.
That's not to say it's inherently a bad thing. But that's at least why it's not inherently a good thing either.
This is the root of all the many issues. There is no standard library. So things that are trivial to write but also dumb to rewrite every time you need them (left-pad a great example) turn into tiny dependencies.
People should not be using these functions at all, in any language, in a standard library or otherwise. To convert the even/oddness of an integer to a boolean in Javascript you can use !!(n % 2), which returns true for odd inputs, and false for even inputs. If you want to be extra explicit you can temporarily store the result of that expression in a meaningfully named variable. If it will just be used in a conditional, then the !! can be skipped.
Depending on a third-party package for this kind of thing is insanity. But the blame lies with programmers who use this package, not with the programming language.
If this is very performance sensitive you can try using the somewhat more obscure (because many people have minimal experience with bitwise operators) `n & 1` instead.
Given an integer input, thare are no "edge cases" to worry about here. If you expect floating point inputs, you need to think carefully about what the correct output should be anyway. A library isn’t going to save you.
(If your "edge case" is that someone might want to know whether a string is even or odd... well, you have other problems.)
If you have a use case where you need to know if you multiplied two large integers and got a result larger than 2^53, then you should do it explicitly before you get to the point of checking if your number is even or odd.
NPM is hands-down the most popular packaging ecosystem online. So of course it has more packages than other ecosystems, more developers use it. It's not a good comparison.
I would not be surprised to find that JS bloat tends to be more severe than other languages, because Javascript makes it a lot easier to import and publish packages. I would also not be surprised if Node package management ended up being a bit worse, again because Node lowers the barrier of entry for inexperienced programmers to publish packages on impulse, even if they don't plan to maintain them. And its just undeniable that Javascript is a heavy intro language, so the average developer quality (especially around responsible dependency management) is probably less than other languages.
But the package count stats need to just die. When we talk about dependency bloat, useful stats would be:
- What is the average/median number of (deduplicated) dependencies in a Node program vs a Rust/Python/Ruby project?
- What is the average/median number of lines of code in a Node package, minus its dependencies? How does that compare to the average/median package in Rust/Python/Ruby?
- What is the average/median number of outdated dependencies in a Node package, vs the average/median in Rust/Python/Ruby?
- What are the average/median number of different authors across a Node package's dependencies vs Rust/Python/Ruby?
- What percentage of the Node ecosystem actually gets used? Are we looking at a long tail of mostly ignored packages, or is usage fairly spread out and diverse?
Heck, even this mostly useless graph would be better if it just adjusted for the number of users for each platform. It's pretty tough get that data, but there are sources you could look at to try and guess, including StackOverflow's dev surveys[0].
The "how many packages are there" metric means nothing when it's quoted in isolation from other data. It's like claiming Windows developers are vastly more productive than Mac developers because more Windows software exists.
[0]: https://insights.stackoverflow.com/survey/2019#overview
It literally takes days to setup shit to get published on maven central and even then you can't set and forget. It's a pain in the ass.
These kind of things are specific for JavaScript because of the mentioned incompatibilities, and of course increase nr packages used. But I'm very happy they exist.
Once you check out a node project with a package.json you can reasonably assume you can install it and run it anywhere. A docker image for a node project should just be "install node && npm build". You're side-stepping the entire dev ops nightmare. Find me another language with JavaScript's popularity and that level of simplicity. I'd go as far to argue that this is what happens to any massively popular language that has a functional package manager from day one.
Find me a mainstream language that's more complicated than this? "Checkout and build" works pretty much everywhere? I mean, you need the right version of mvn or pip or whatnot... just like you need the right version of npm.
If anything, node/npm deserves special mention for NOT having reliable repeatable builds. Part of this is that a lot of packages rely on native code. But most of it is that the ecosystem culture defaults to "always grab the latest versions of everything".
And npm only got package-lock.json a few years ago, not "from day one". Prior to package-lock.json, builds were wildly unpredictable - like, expect breaking changes week to week even if you don't change anything at all in your code.
If you want an "entirely self contained" payoff, languages that produce static binaries are pretty hard to beat. Node is not that.
Nowadays on javaland you should only need to have JDK and git/svn/hg/(wtv version control you use). And with the JDK, you can always download the most recent that java is backwards compatible.
Are you actually arguing over the state of JS development from 4 years ago?
For a Node project you just need node. npm is part of node, and yarn is optional. The repeatable builds was a real issue but that's been solved years ago.
Npm's dev dependencies are analogous to Maven's plugins.
Bazel is an alternative to maven. Docker fulfills the same role as in the Node ecosystem. The runtime class path is an implementation detail that you need to concern yourself with if you wish to run your Java code without Maven (analogous to running your JavaScript code without npm).
The two forms of the same dependency is a limitation that stems from Java the language rather than its ecosystem.
FWIW, nearly every language ecosystem I know of is at the point of "download build tool, run build tool."
I recently had a case where a developer joined us on a project and he got a different version of a package than I did because the lockfile didn’t constraint the dependencies and sub-dependencies and everything else. (For that you have to pass an explicit parameter like `--ci` or `--frozen-lockfile` depending on which of three different package managers you use.)
Bollocks, I say.
That defeats the purpose of having separate dev and runtime dependencies and results in a huge docker image filled with dev-time dependencies.
Also, other package managers do this fairly well. Java has maven and uber-jars (for self contained artifacts), python has pip and venv, etc.
no such problems in C, i promise you
"small packages are extremely common in the JavaScript npm package system. For example, in npm, 47% of the packages have 0 or 1 functions, and the average npm package has 112 physical lines of code. In contrast, the average Python module in the PyPI repository has 2,232 physical lines of code."
Source: "Vulnerabilities in the Core: Preliminary Report and Census II of Open Source Software", The Linux Foundation & The Laboratory for Innovation Science at Harvard, by Frank Nagle, Jessica Wilkerson, James Dana, Jennifer L. Hoffman. https://www.coreinfrastructure.org/programs/census-program-i...
(This is a contrast of citations from other works; you can see the specific citations in the report, but I'm trying to do this from a phone. In any case, the issue is the massive difference between JavaScript and everything else, and I think this quotation shows it nicely.)
One of the reasons is that the iOS SDK is really complete by itself while Javascript doesn't even ship with a usable Date implementation. If somebody could just come up with a standard library for JavaScript that is really widely adopted I think the amount of packages can be cut back dramatically.
(1) totally based on gut feeling, not based on hard data
> If somebody could just come up with a standard library for JavaScript that is really widely adopted I think the amount of packages can be cut back dramatically.
As this article points out, I think that lodash is that for many people.
Get your JS Core libraries once from a repo, reuse them in every application that uses the same ones
JS’s version of Date in comparison has always been a toy.
So the Java community coalesced around Joda time, not unlike how the JS community coalesced around moment.js. The big difference is that yes, Java eventually rolled these lessons learned into the java.time API, but now you're effectively stuck with 2 standards, Joda and java.time. Not sure if that's much of an improvement.
Because of the small stdlib, you need dependancies, sure.
But why this idea that each dependency must be very small, forcing you to install a hundreds childs, which, themselves, will install a hundred until the pyramid of files start to turn node_modules into a stress test?
We should have a de facto standard library that gets distributed along with the language's reference implementation. Widely-used packages that have a stable and well-designed interfaces should be distributed by default. If something isn't being used anymore, it can be dropped from the standard library while still being available from the repositories.
To me, culture is a feature, and despite using JS a lot since it has a monopoly on the browser, the community culture never clicked with me.
I often feel like they are reinventing the wheel, not learning from other techs solving the same problems, under and over-engineering, regularly guilty of the XKCD 927 syndrom.
Few projects taste integrated, working together, clean. The stacks looked like hacked together. Agreement on best practices and conventions are timidly showing up. It also seems popular tools are just starting to stabilize and the breaking modifications rhythm to slow down. At least more than anywhere else I worked.
There is no way I can be objective saying this, but anyone with the same feeling?
1) JavaScript is a first language for many, many new developers, especially the ones not coming from a formal computer science program. This is because it's multi-paradigm, extremely hireable, runs on everything, runs on the web specifically, and other reasons. If you're only going to learn one language, it's a very good and versatile choice. So its community has a lot of inexperienced members.
2) Because it's such a focal point of the industry, it gets a lot of attention from major players as well and has been evolving rapidly in terms of core features. These features usually get tested out first in the form of packages.
3) Being a dynamic language it's very easy to extend and hack. The prototype system even allows you to modify the behavior of its core primitives.
4) The more experienced devs who use it are often dissatisfied with the qualities of the base language, and so take advantage of its flexibility to try and extend it into what they think a good language is (see TypeScript, Ramda, RxJS, etc).
The response to the leftpad scandal was for NPM to disallow packages being unpublished once they've been out for 72 hours, which really should've always been the case.
Widely used packages should form the basis of a standard library that is distributed with the language itself.
Small, separately maintained packages are bad in a an ecosystem where a final system can incorporate only one version of any given package because it increases the number of opportunities for version conflicts.
Now, it's true that there are some other concerns which weigh in favor of packages being at the minimum useful size, but those necessarily also weigh in favor of a package management system which allows each package to isolate its upstream dependencies without constraining it's downstream users. And not just allows, but facilitates it so that it is the norm, so that dependency conflicts aren't a thing.
Personally, I've made it a habit to not add anything that requires trivial one liner garbage packages, which amounts to not installing any dependency that uses anything from Jon Schlinkert (so no Webpack). Unfortunately I'm stuck with gulp right now but with the next major rewrite, I'll also get rid of gulp and its 300 dependencies.
Then the problem is the fact anybody can submit a package, not small packages.
Linux distributions solve this problem by having dedicated maintainers. Users of a distribution trust its maintainers when they use it. Software developers want the complete opposite: language-specific packages, instant and unrestricted package publication, containers, etc. Nobody wants to have to talk to a maintainer in order to get their software included in a distribution. Of course the result ends up being a mess.
What if Node's maintainers decided to distribute a curated set of packages alongside Node itself? The size of each individual package wouldn't really matter.
First, front-end has nothing to do with Node. Secondly, let’s say there’s some sort of front-end foundation who decides to provide a curated set of packages. A natural candidate would be create-react-app, which pulls over a thousand dependencies. That’s just one package you want to include. How the hell do you even start to curate such a beast?
It was just an example.
> A natural candidate would be create-react-app, which pulls over a thousand dependencies. That’s just one package you want to include. How the hell do you even start to curate such a beast?
I don't know. How did Linux distributions do it? Arch Linux has over 10 thousand packages. Debian has over 50 thousand packages. Maybe people should ask the maintainers.
Oh and I was a maintainer for a major package manager back in the day for crying out loud (was responsible for overseeing the general health of the project as well as directly responsible for maybe a couple dozen individual packages). Never seen this kind of madness.
https://www.archlinux.org/groups/x86_64/kde-applications/
https://www.archlinux.org/groups/x86_64/pro-audio/
The Arch Wiki recommends the use of these huge packages. I don't see any reason why it wouldn't scale to a thousand packages or more.
- makes dependency resolution harder
- means more points of failure
- makes auditing difficult
- makes install time longer
- leads to harder maintenance and upgrade
This also causes heterogeneity in the mass of your dependencies:
- it splits the resources for documentation, testing, tutorial, etc
- it ensures very weak integration between various building blocks, forcing everyone to rebuild glue code again and again
- it makes discovering the proper solution to your problem harder as you must chose a combination of dependencies instead of one
- it makes contributing to the project harder, especially to junior profiles, and increases the price of on-boarding
- eventually, it leads to a culture that pushes the "libs versus frameworks" so far you never get a decent framework for anything. This is why there is no JS equivalent to RoR or Django.
There is, of course, a balance to reach. You don't want a lib that does everything.
In the article 19000 packages are installed in 40 seconds... My Django project with 100 times fewer dependencies takes longer in CI
Npm comes with an audit tool
I have never seen dependency resolution fail (since packages can have private copies), unlike pypi or rubygems.
So some of the downsides to a large dependency tree are mitigated. I'll add one more downside:
- more chances for shenanigans like left-pad to cause issues.
Don't get me wrong, I think 19000 dependencies is fucking nuts. NINETEEN THOUSAND.
Doesn't this happen because of package manager duplication? I think npm lets packages have their own copies of their dependencies. Since we have a lots of small, widely used packages, they get duplicated at numerous points in the dependency graph. The number of files and installation size explodes.
I started a vue project last night, npm install --production installs 4 packages. With dev dependencies, I get 2300 packages. Eslint, babel, webpack, etc bring in lots of luggage
BTW, I think 19000 is wrong, on a fresh node_modules I get:
$ npm install gatsby
...
+ gatsby@2.20.18
added 1773 packages from 733 contributors
and audited 23706 packages in 52.317s
$ du -sh node_modules
245M node_modules
Not sure where the "audited" number comes from, but its not the number of install packages. I get 2737 directories containing a package.json, 1477 of which are uniq.`debug` appears 32 times!
It's just so absurd and nonsensical, nobody can understand what that even mean.
Seems to me to be a crazy huge liability, but quite a lot of people seem to be fine with that.
I don't know what to do from that information, but trying to create some awareness around the issue feels like talking about the number of operations per second a CPU does with someone who has zero idea what a realistic range is. It's seen as a high number, but doesn't have any meaning attached to it.
Edit: I don't have access to the project anymore to check more details (such as installation time), but I checked some personal notes I took when I found this out, the redacted output from npm audit is the following
$ npm audit
<REDACTED> vulnerabilities found - Packages audited: 927016
Severity: <REDACTED> Low | <REDACTED> Moderate
Done in 41.62s.
So slightly below the 1 million mark.Edit 2: Now I'm wondering ... maybe the "packages audited" from npm audit doesn't mean "you have that number of packages, and I audited them". But if that's the case, that's a terrible UX, and I have zero idea what that number mean, which kind of support my point.
This doesn't make even the slightest bit of sense. Did somebody somehow just import... everything by accident? Did npm hit a bug? Is there some infinite recursion going on that eventually hits a wall?
This obviously isn't representative.
How are there 1 million interesting packages?!
In my job we use like, I don't know, 20 libraries total, out of, say, 50 alternatives that could do the job. Including dependencies of the dependencies I don't think we reach anywhere near 100. Every dependency is discussed with the entire team when we add it.
How do you even have time to manage 1 million?!
I still can't fathom getting to a million though.
"Luckily, most of them are using the same version of lodash, which just needs one lodash to install inside node_modules. This is often not the case with real-world production projects. Sometimes, different packages require different versions of other packages."
Npm lets you install several versions of the same package. This has the advantage of saving the dev the testing of their lib with a wide range of dependencies versions: it almost always works. Unfortunately, that means most devs just choose one version and call it a day. They build libs like one would build a project: as you were alone in the world.
So there's that too.
I don't love NPM anymore than the next guy, but my blame will go to the dev in this case.
Cargo, Mix, and Ruby’s Bundler _all_ do an infinitely better job because they don’t let dependencies upgrade on you behind the scene. Their lockfiles are really lockfiles. No ifs, ands, or buts.
`--frozen-lockfile` and the equivalent should be the DEFAULT behaviour, not this pseudo-locked nonsense that currently exists.
A bigger issue with all of it is that in my line of work, for the backend of firmware I cannot just depend on 3rd party libs as they will get audited, so I need to audit them before. Ofcourse most people don't have that issue.
`npm i && npm start` is as good as it gets in terms of "getting started" friction (with version locking): python is worse, ruby is worse, PHP is worse, Java is worse.
React-Native has more moving pieces indeed, but it's not really JS's (or NPM's) fault: you get all problems of iOS and Android at once. I will definitely agree with you that working on React-Native is a pain, to be able to work with modern tools need to know JS, optionally TS, Obj-C, Swift, Java, Kotlin, Ruby (for Fastlane) and Gradle's custom language. Insane.
- go build ./... builds everything from the current project
- go test ./... tests everything from the current project
- go install ./... installs everything from the current project
That's one of the best thing ever. I can go to any project, I know how to build, test, install, and can directly be productive.
On the other hand, I started learning C++ one year ago, and it's the exact opposite, each project has its own homemade build system that isn't supported by one platform or the other.
Yes, we get comically large numbers when we look through node_modules. But, real "dependency hell" is when you have situations that take unbounded manual effort to resolve.
How often do we end up with impossible circular dependencies? Or conflicting versions clobbering each other? Or non-deterministic builds introducing random behavior?
That all is commonplace even today with other platforms like python, and rarely an issue with node. I'd much rather occasionally sneer at the size of node_modules than any of that actual hell.
I was shocked this behavior exists when a site broke by upgrading a sub dependency. Turned out they both installed react so they used a different “creatContext” and now there were two context instances instead of one.
If not, NPM module flattening should take care of it.
Then worst case, you can also tell webpack to bundle a specific installation.
There are some ways to solve these problems...
- function/object identity checking I.e “package.func !== package.func” in many cases places.
- module level singletons and registry (e.g createContext useContext)
- single initialization
- others I can’t think of
That’s not typical and I was surprised by it.
If more libraries were maintained by groups in a shared/collaborative way, more of those risks would go away.
I think maybe the best solution we have so far are robust "batteries included" standard libraries like python, where even if they're huge theyre at least curated and maintained by a central group.
I would be so incredibly grateful to have something comparable in JavaScript that quickly checks static typing for my entire project. But the closest I have found is Rust/Elm, so not using JavaScript anymore.
JavaScript is great when it's used for enabling interactive websites, but nowadays more and more websites use it to break features, which don't need JavaScript at all.
jekyll has 13
I don't disagree that lots of deps = supply chain risk
But there have always been variations between projects & kinds of projects re how many deps they pull in. Try following someone's ipython data science tutorial, it's requirements.txt for days
I think lack of a standard lib early on plus hipster functional culture made the '10s JS leaders like small monofunctional libs.
Focus on transpilation plus lack of tree-shaking in early versions of webpack may have also created an incentive for small things.
I see 4 dependencies in the setup.py, and they are all from the same team of maintainers (i.e the Pallet team):
- Werkzeug
- Jinja2
- itsdangerous
- click
sanic = 18, fastapi = 3 but one of them is a 7MB download (pydantic)
yeah it's less, but also python has an amazing stdlib
That said, 4 dependencies is the norm in Python land, whereas 13 dependencies is an outlier in JS land.
Edit: oops, misread, gp said express has 51 deps, not 13.
There's been a TC39 proposal for a JS stdlib for about 2 years now: https://github.com/tc39/proposal-javascript-standard-library
One of the things I love about Go is generally people are a little more forgiving to copying something around a couple times instead of making a lib for everything.
Come to think of it, something that does for packages what jQuery did to bring (some) sanity to browser incompatibilities.
In fact this is the root of the issue. It's important to realize that `node_modules` for any project in Popular Framework X will be almost identical. Most of the packages are related to compiling, bundling, and the development environment and a small minority are actual client-side dependencies. Even the frameworks themselves are pretty light -- React has a grand total of 2 dependencies. It's packaging the entire toolchain that causes most of the bloat.
Many a time I've forked a dependency for one reason or another, and kept it up to date with upstream. Sure, it's a few more steps to update, but allows for deep customizations without waiting for original authors to support it (if ever).
Another reason to fork/copy code, is that its development has ceased long ago. In that case, it totally makes sense to carry on the work, in-house at first.
Regarding the article, sometimes copying code keeps the entire codebase easier to understand and manage without having an external dependency.
I complete agree. To temper my comment, I've seen plenty of bad examples of copying dependencies, bundling old un-updated versions, with undocumented changes. In such case, it's true that copying leads to a worse kind of dependency.
1. A not insignificant number of packages are either polyfilling things in browsers, or providing a consistent (or ‘isomorphic') API for certain things across both browsers and Node.js, or adding things that should have been in a standard library.
2. A lot of packages still seem to be distributed primarily as CommonJS and not ES Modules. CommonJS makes tree-shaking harder than it should, so it was often just easier to break what should have been a single library into many smaller pieces.
Hopefully in the not too distant future library maintainers get around to reading the Node.js docs[0] fix some of this.
JavaScript is super popular and used by companies so there are lots of people releasing their own open source project in an attempt to hustle into a well-paid job by looking more experienced than they really are.
I'd say it's the same reason why there are so many bad Java and Php tutorials.
C++ for comparison is shielded from this because it's not the kind of programming language that you would learn in a weekend seminar to improve your salary.
A reasonable complaint! Instead of npm, try pnpm [0]. It uses file links to avoid duplication.
Is it performance (I haven't experienced it)? Is it taking up disk space (I have terabytes to spare)? Does it add complexity (I would argue it reduces complexity)?
What exactly is the problem?
Try installing a package for Rust. Or Go. Or ... any language, really.
As I wrote in a sibling comment, I have a project I haven't even started yet in Rust [1]. And it's measly 6 dependencies pull a total of 197.
197 dependencies is too many to trust.
The largest, and not exactly admirable, I can find is reqwest that drags in 97.
Whilst 100 is a huge number... It's an enormous gap from the many thousands.
I have a project I haven't even started yet in Rust. Three dependencies in Cargo.toml. They download and install 34 dependencies.
I've now added three more (from kube-rs readme). It's now 197 dependencies. And so on.
I have a small project that I work on from time to time which uses 5 libraries (react, a map library and a chart library), typescript, and react-scripts (which I guess pulls in webpack and all the rest).
This is what happens when I run a npm audit (I havent' touched it for a couple of months)
> found 38934 vulnerabilities (38916 low, 18 moderate) in 906346 scanned packages
> run `npm audit fix` to fix 38620 of them.
> 314 vulnerabilities require manual review. See the full report for details.
While the real numbers are probably lower because there is a lot of duplication inside the node_modules folder, I find this ... astounding.
(1) in fact, seeing some of the worst kept me away from python for far too long until I realized plenty of python software doesn’t even need pypi and most only need it for a couple things with no transitive dependencies
There is perhaps a something about the JavaScript audience tending to attract less advanced implementers. So you end up with a lot of otherwise considered trivial and low-quality utilities being published and pulled down from the registry.
Rust and Go tend to be for more advanced scenarios where things like performance, security and support matters tremendously.
I feel like we need to really think about the consequences of making these kinds of technical choices, especially in 2020 given all the horror stories circulating. Someone is eventually going to have to come in after the fact and scrape all the gore off the walls and redo your webapp in .NET/Rust/Go/etc. Why not start there and be done with it? You could save so much frustration. Is it about subjective/artistic preferences? At what point should the business owner start getting involved in these technical decisions? If the business owner found out their webapp was trapped in hell because you had a personal aesthetic attraction to one language over another much more pragmatic language, how do you think they would respond?
My personal feeling about CRUD is this, you don't have to get fully on the crazy train and spend all your time with packages and build tools to achieve a nice product. Yarn is better for me. Very sparing package use. And a little light Vue with server side templating. No I don't need you managing routes there JS framework, that never was a good idea in my estimation. (Or maybe it is and I don't get it but I like incremental changes and simplicity).
Once the dust settles perhaps I'll move on to a more complex setup but I don't have time for it now. I'm trying to get stuff done. However this doesn't have to mean eliminating modern behavior or appearance. Which means some JavaScript. It's going to be required in general by end users.