Having clearly defined and delineated build processes is key when it comes to successfully managing an application across development, staging, and production environments. Each of these commonplace environments, and other environments you might encounter, is used for a specific purpose and benefits from being geared towards that purpose.
For development, we focus on enhanced debugging facilities, using development versions of libraries, source maps, and verbose logging levels; custom ways of overriding behavior, so that we can easily mimic how the production environment would look like, and where possible we also throw in a real-time debugging server that takes care of restarting our application when code changes, applying CSS changes without refreshing the page, and so on.
Production focuses more heavily on minification, optimizing images statically to reduce their byte size, and advanced techniques like route-based bundle splitting, where we only serve modules that are actually used by the pages visited by a user; tree shaking, where we statically analyze our module graph and remove functions that aren’t being used; critical CSS inlining, where we precompute the most frequently used CSS styles so that we can inline them in the page and defer the rest of the styles to an asynchronous model that has a quicker time to interactive; and security features, such as a hardened policy that mitigates attack vectors like XSS or CSRF.
Testing also plays a significant role when it comes to processes around an application. Testing is typically done in two different stages. Locally, developers test before a build, making sure linters don’t produce any errors or that tests aren’t failing. Then, before merging code into the mainline repository, we often run tests in a continuous integration (CI) environment to ensure we don’t merge broken code into our application. When it comes to CI, we start off by building our application, and then test against that, making sure the compiled application is in order.
As we’ll extensively discuss in the fourth book in the Modular JavaScript series, there are numerous services that can aid with the CI process. Travis[] offers a quick way to get started integration testing your applications by connecting to your project’s repository and running a command of your choosing, where an exit code of means the CI job passes and a different exit code will mean the CI job failed. Codecov[4] can help out on the code coverage side, ensuring most code paths in our application logic are covered by test cases. Solutions like WebPageTest[], PageSpeed[6], and Lighthouse[] can be integrated into the CI process we run on a platform like Travis to ensure that changes to our web applications don’t have a negative impact on performance. Running these hooks on every commit and even in Pull Request branches can help keep bugs and regressions out of the mainline of your applications, and thus out of staging and production environments.
Note how up until this point we have focused on how we build and test our assets, but not how we deploy them. These two processes, build and deployment, are closely related but they shouldn’t be intertwined. A clearly isolated build process where we end up with a packaged application we can easily deploy, and a deployment process that takes care of the specifics regardless of whether you’re deploying to your own local environment, or to a hosted staging or production environment, means that for the most part we won’t need to worry about environments during our build processes nor at runtime.