The frontend ecosystem is notoriously confusing. At every layer, there seem to be incompatible, competing standards.
No universal import system. ESModules, CommonJS, Asynchronous Module Definition (AMD), and Universal Module Definition (UMD) are all different ways you can import or share your code. Bundlers try and solve some of this by supporting multiple methods. But
Layers of minification, uglification, and transpilation. Code undergoes multiple transformations before it’s ran. TypeScript gets compiled into JavaScript. Code on the web gets minified (to reduce network bandwidth) or uglified (to prevent copying). Source maps fix some of this, but it’s another thing to configure. Getting the right stack trace can be tricky. It requires coordination between the languages, the tools, and the runtimes.
Wildly different environments. A feature and a bug. Frontend code is expected to run everywhere – not just the browser. The context and APIs available in different environments vary, and it’s hard to know what context you have available (and even harder to know what libraries you’re importing assume). Can this code run on the server? Can this code run on the client? This is tough for developers (what code can I use) and for library maintainers (what environments should I optimize my code for?).
Overemphasis on file structure. Too many frontend tools rely on the project structure for behavior. Configuration that must be in the project’s root directory (leading to a long list of tailwind.config.js, postcss.config.js, eslint.config.js, next.config.js, and more). File structure is a necessary evil for importing code, but it ends up doing much more in frontend. It might be an API to route a particular file as a webpage, or as an API, or as a static webpage, or as a dynamically regenerated webpage. These are convenient, but sometimes hard to debug and hard to discover parts of a codebase.
Configuration hell. There are so many tools to use out of the box. For a long time, there was create-react-app, which was a blessed bundle of many of these tools that gave developers a working configuration from the start. But if you veered off the golden path, you were left with 20+ developer tools with complex interactions. Almost every tool fights each other. ESLint (linter) and Prettier (formatter) often conflict.
Development parity. Having so many steps between code and deployment means that hot-reloading development is often complicated. This leads to tools like webpack-dev-server, which handles most of that for you. But beware of magic. There are so many assumptions in these development servers that they might diverge from production behavior quickly.