DevTools architecture refresh: Modernizing CSS infrastructure in DevTools
This post is part of a series of blog posts describing the changes we are making to DevTools' architecture and how it is built. We will explain how CSS worked in DevTools historically and how we’ve modernized our CSS in DevTools in preparation for (eventually) migrating to a web standard solution for loading CSS in JavaScript files.
Previous State of CSS in DevTools
DevTools implemented CSS in two different ways: one for CSS files used in the legacy part of DevTools, one for the modern web components that are being used in DevTools.
The CSS implementation in DevTools was defined many years ago and is now outdated. DevTools has stuck to using the module.json
pattern and there has been a huge effort in removing these files. The last blocker for removal of these files is the resources
section, which is used to load in CSS files.
We wanted to spend time exploring different potential solutions that could eventually morph into CSS Module Scripts. The aim was to remove technical debt caused by the legacy system but also make the migration process to CSS Module Scripts easier.
Any CSS files that were in DevTools were considered to be ‘legacy’ as they were loaded using a module.json
file, which is in the process of being removed. All CSS files had to be listed under resources
in a module.json
file in the same directory as the CSS file.
An example of a remaining module.json
file:
{
"resources": [
"serviceWorkersView.css",
"serviceWorkerUpdateCycleView.css"
]
}
These CSS files would then populate a global object map called Root.Runtime.cachedResources
as a mapping from a path to their contents. In order to add styles into DevTools, you would need to call registerRequiredCSS
with the exact path to the file you want to load.
Example registerRequiredCSS
call:
constructor() {
…
this.registerRequiredCSS('ui/legacy/components/quick_open/filteredListWidget.css');
…
}
This would retrieve the contents of the CSS file and insert it as a <style>
element into the page using the appendStyle
function:.
appendStyle
function that adds CSS using an inline style element:
const content = Root.Runtime.cachedResources.get(cssFile) || '';
if (!content) {
console.error(cssFile + ' not preloaded. Check module.json');
}
const styleElement = document.createElement('style');
styleElement.textContent = content;
node.appendChild(styleElement);
When we introduced modern web components (using custom elements), we decided initially to use CSS via inline <style>
tags in the component files themselves. This presented its own challenges:
- Lack of syntax highlight support. Plugins that provide syntax highlighting for inline CSS do not tend to be as good as the syntax highlighting and auto complete features for CSS written in
.css
files. - Build performance overhead. Inline CSS also meant that there needed to be two passes for linting: one for CSS files and one for inline CSS. This was a performance overhead we could remove if all CSS was written in standalone CSS files.
- Challenge in minification. Inline CSS could not be easily minified, so none of the CSS was minified. The file size of the release build of DevTools was also increased by the duplicated CSS introduced by multiple instances of the same web component.
The goal of my internship project was to find a solution for the CSS infrastructure that works with both the legacy infrastructure and the new web components being used in DevTools.
Researching potential solutions
The problem could be split into two different parts:
- Figuring out how the build system deals with CSS files.
- Figuring out how the CSS files are imported and utilised by DevTools.
We looked into different potential solutions for each part and these are outlined below.
Importing CSS Files
The goal with importing and utilising CSS in the TypeScript files was to stick as close to web standards as possible, enforce consistency throughout DevTools and avoid duplicated CSS in our HTML. We also wanted to be able to pick a solution that would make it possible to migrate our changes to new web platform standards, such as CSS Module Scripts.
For these reasons the @import statements and tags did not seem like the right fit for DevTools. They would not be uniform with imports throughout the rest of DevTools and result in a Flash Of Unstyled Content (FOUC). The migration to CSS Module Scripts would be harder because the imports would have to be explicitly added and dealt with differently than they would with <link>
tags.
const output = LitHtml.html`
<style> @import "css/styles.css"; </style>
<button> Hello world </button>`
const output = LitHtml.html`
<link rel="stylesheet" href="styles.css">
<button> Hello World </button>`
Potential solutions using @import
or <link>
.
Instead we opted to find a way to import the CSS file as a CSSStyleSheet
object so that we can add it to the Shadow Dom (DevTools uses Shadow DOM for a couple of years now) using its adoptedStyleSheets
property.
Bundler options
We needed a way to convert CSS files into a CSSStyleSheet
object so that we could easily manipulate it in the TypeScript file. We considered both Rollup and webpack as potential bundlers to do this transformation for us. DevTools already uses Rollup in its production build, but adding either bundler to the production build could have potential performance issues when working with our current build system. Our integration with the GN build system of Chromium makes bundling more difficult and therefore bundlers tend not to integrate well with the current Chromium build system.
Instead, we explored the option to use the current GN build system to do this transformation for us instead.
The new infrastructure of using CSS in DevTools
The new solution involves using adoptedStyleSheets
to add styles to a particular Shadow DOM while using the GN build system to generate CSSStyleSheet objects that can be adopted by a document
or a ShadowRoot
.
// CustomButton.ts
// Import the CSS style sheet contents from a JS file generated from CSS
import customButtonStyles from './customButton.css.js';
import otherStyles from './otherStyles.css.js';
export class CustomButton extends HTMLElement{
…
connectedCallback(): void {
// Add the styles to the shadow root scope
this.shadow.adoptedStyleSheets = [customButtonStyles, otherStyles];
}
}
Using adoptedStyleSheets
has multiple benefits including:
- It is in progress of becoming a modern web standard
- Prevents duplicate CSS
- Applies styles only to a Shadow DOM and this avoids any issues caused by duplicate class names or ID selectors in CSS files
- Easy to migrate to future web standards such as CSS Module Scripts and Import Assertions
The only caveat to the solution was that the import
statements required the .css.js
file to be imported. To let GN generate a CSS file during building, we wrote the generate_css_js_files.js
script. The build system now processes every CSS file and transforms it to a JavaScript file that by default exports a CSSStyleSheet
object. This is great as we can import the CSS file and adopt it easily. Furthermore, we can also now minify the production build easily, saving file size:
const styles = new CSSStyleSheet();
styles.replaceSync(
// In production, we also minify our CSS styles
/`${isDebug ? output : cleanCSS.minify(output).styles}
/*# sourceURL=${fileName} */`/
);
export default styles;
Example generated iconButton.css.js
from the script.
Migrating legacy code using ESLint rules
While the web components could be easily manually migrated, the process for migrating legacy usages of registerRequiredCSS
was more involved. The two main functions that registered legacy styles were registerRequiredCSS
and createShadowRootWithCoreStyles
. We decided that since the steps to migrate these calls were fairly mechanical, we could use ESLint rules to apply fixes and automatically migrate legacy code. DevTools already uses a number of custom rules specific for the DevTools codebase. This was helpful as ESLint already parses the code into an Abstract Syntax Tree(abbr. AST) and we could query the particular call nodes that were calls to registering CSS.
The biggest issue we faced when writing the migration ESLint Rules was capturing edge cases. We wanted to make sure we got the right balance between knowing which edge cases were worth capturing and which should be migrated manually. We also wanted to be able to ensure that we could tell a user when an imported .css.js
file is not being automatically generated by the build system as this prevents any file not found errors on runtime.
One disadvantage of using ESLint rules for the migration was that we could not change the required GN build file in the system. These changes had to be manually done by the user in each directory. While this required more work, it was a good way of confirming that every .css.js
file being imported is actually generated by the build system.
Overall, using ESLint rules for this migration was really helpful as we were able to rapidly migrate the legacy code to the new infrastructure and having the AST readily available meant we could also handle multiple edge cases in the rule and reliably automatically fix them using ESLint’s fixer API.
What next?
So far, all the web components in Chromium DevTools have been migrated to use the new CSS infrastructure instead of using inline styles. Most of the legacy usages of registerRequiredCSS
have also been migrated to use the new system. All that is left is to remove as many module.json
files as possible and then migrate this current infrastructure to implement CSS Module Scripts in the future!
Download the preview channels
Consider using the Chrome Canary, Dev, or Beta as your default development browser. These preview channels give you access to the latest DevTools features, let you test cutting-edge web platform APIs, and help you find issues on your site before your users do!
Get in touch with the Chrome DevTools team
Use the following options to discuss the new features, updates, or anything else related to DevTools.
- Submit feedback and feature requests to us at crbug.com.
- Report a DevTools issue using the More options > Help > Report a DevTools issue in DevTools.
- Tweet at @ChromeDevTools.
- Leave comments on What's new in DevTools YouTube videos or DevTools Tips YouTube videos.