How to refactor 17 thousand lines of CSS

    Many of us are working on large projects. The one I will talk about has been living for 15 years and has included a couple of dozen web applications on ASP.NET WebForms, the main of which contains about one and a half thousand aspx pages.

    Why so many? The desire to adapt to customers with different requirements makes itself felt. Everyone wants some special functionality of their own and eventually gets it. But this is not about that. In addition to a large number of pages, we had many CSS styles. Lots of.

    A picture to attract attention

    Source: Ursus Wehrli. The Art Of Clean Up


    Initially, there was one web project. He had one CSS file. The file was not too large, and its readability did not raise any questions. Time passed, people came, created new functionality and added selectors to the CSS file. It didn’t look very scary, you could still figure out the contents.

    Then came the time for complex functionality, when the styles of controls located on certain pages overlapped, which hung a certain class. Designers and users liked the UI, and the class migrated to other pages without any semantic connection with the original place. At some point, the file turned into a giant garbage dump of 17 thousand lines.

    In order not to seem too simple, the system came up with custom skins (separate styles) for individual customers. These 17 thousand lines are common for everyone, but we will take out a little more to customize the colors of the text and background. In fact, it turned out quite a bit, but very much. Kilometers of CSS, and some part of them could well be common to all.

    Problem


    We knew that despite the apparent visual integrity of the design, the project has many exceptions to the rules. Here are some examples:
    • 12 different grids that look not so much like, but not so much different.
    • 4 options for toolbars over grids with three options for buttons and drop-down lists (also different, but suspiciously similar in style).
    • Even informational messages, which are just text with an icon on a red, yellow or blue background, and there were 2 options.

    There is one more thing to be said. We have not one web project, but several. The controls and styles for each of them developed in parallel for quite a long time, when, finally, someone had no idea the idea of ​​combining all this and putting it into the library of general controls with a part of the common styles (which can then be overlapped in each of the projects). Writing this library did not mark the unification of everything and everything and the removal of duplicates. Instead, there are even more controls and styles. A shared grid in a shared library is a useful thing, but you cannot remove 12 available from three hundred places throughout the project - not a single normal team will subscribe to it. In general, at some point, people ceased to understand which of our many controls should be used in a particular case.

    And we also came across a well-known problem in Internet Explorer 9, which for some reason refused to accept more than 4095 selectors in our glorious file.

    And so we had to completely change the whole design of the application. It was clear that just changing the design would be more expensive for yourself - you had to refactor CSS first. All CSS. Product owners understood the situation and we got the go-ahead for refactoring.

    Getting started


    So, we all agreed and got freedom of action - we begin to rake up the rubble. What do we have in stock?
    • One giant CSS file with 17 thousand lines for the main web application
    • More than 50 custom skins, for each a separate CSS file. In theory, only minor differences should be present in it; in practice, quite fraught pieces of common code were encountered.
    • CSS file with a library of controls, which was used not only in the main, but also in other applications.
    • CSS files for additional applications. Everything was not difficult here, if only because they were all, albeit with copy-paste, but rather small

    The first thing we started with is removing unused styles. With C # code, everything is simple, the solution analyzer is turned on, and it showed which of the public methods can be removed. With styles, the only way is a text search by solution. If you are ever asked to clean out 17 thousand lines in a CSS file that is in a solution of two hundred and fifty projects in this way, ask yourself for an SSD drive. The process will go a little faster.

    But if you are just planning to write such a gigantic project from scratch, then here are some tips for you:

    Always use the name of the selector in the code in its entirety.


    Never break it into pieces, for example:

    public const string ProgressBar = "progressbar";
    public const string ProgressBarSmall = ProgressBar + "small";
    

    You will find a large progress bar, and delete a small one as unused during refactoring. Then you have to restore. You think, and so remember what is used and what is not? In a project of one and a half thousand pages? And with hundreds of settings that enable and disable various features?

    Also, do not be clever with constructions of this kind:
    public class Feedback
    {
        public static string CssClass = GetType().ToString();
    }
    

    Firstly, for class heirs it still doesn’t work ( GetType()you need to change it to typeof(Feedback)), and secondly, it is impossible to search.

    Just keep the text. Search text in solution with preserve case and whole word. Do not complicate your life.

    Use prefixes.


    About this it is worth talking separately below. For now, let’s omit the ability to conditionally divide selectors belonging to different modules, giving them separate prefixes.

    You cannot search for CSS classes with the following names:
    .hidden
    .visible
    .error
    .text
    

    Much nicer with prefixes. The first two can be conditionally assigned to helpers - helpers. Give them prefixes: .h-hiddenand .h-visible. Already you can search! If the classes are specific, then you can use the name of the project. MyProject? Let it be .mp-. Let's say .mp-login-page. Are the controls common? - .ct-

    Some third-party libraries also use prefixes. By prefix, it becomes easy to distinguish what the code belongs to. In addition, a small risk of name intersection is eliminated.

    Do not use the same selector names for different needs.


    For example .errorand .textin the previous example - is very advanced case. Selectors are not always unique and can be used in completely different ways in different cases.
    .mp-login-page .error
    {
        color:red;
    }
    .ct-feedback .error
    {
        background-color: red;
    }
    

    I would replace this with the following selectors:
    .mp-login-page-error
    {
        color:red;
    }
    .ct-feedback-error
    {
        background-color: red;
    }
    

    Cumbersome? Undoubtedly. But it is well sought and removed.

    Make yourself a list of styles.


    Make yourself a static class specifying if not all, then at least frequently used styles. It will be much easier and then search and rename.

    Acting


    My uncle always said that already at the construction stage it is necessary to think about how to make it so that if desired, all this could be most easily broken.

    Do you think your project is small and you don’t have to? We also did not think, and the project turned into a very big monster . In general, deleting unused styles is quite a normal situation. There were many unused classes, often it was possible to delete several screens of text in a row. As a result, only 17 thousand lines remained in our file of 17 thousand lines. Is it true that we use all this? Yes! This is our everything acquired over the years. Everything is needed!

    What's next? It turned out that we still have a lot of selectors left, and after cleaning all 100% of them are used. When we deleted unused styles, we paid attention to such selectors, in which the class name was found only once
    .personal-report-bottom-section
    {
        margin-top: 10px;
    }
    .users-overview-header
    {
        padding-left: 15px;
    }
    

    It turned out somehow completely stupid. A bunch of similar styles in classes used once. The idea was borrowed from Bootstrap to use helper classes. Let's say for margin-top: 10pxyou can use the name .h-mt10, for padding-left: 15px;- .h-pdl15. This helped clear out many more places.

    Then they began to search for pieces that were repeated in meaning. The most popular was to have a hyperlink or plain text with a picture on the left ():
    a.ct-imagewithtext
    {
        text-decoration: none;
    }
    .ct-imagewithtext img,
    .ct-imagewithtext span
    {
        vertical-align: middle;
    }
    .ct-imagewithtext img
    {
        margin-right: 4px;
    }
    .ct-imagewithtext span
    {
        text-decoration: underline;
    }
    

    I think that there were 20 similar styles in the code, no less. But each time the class names were new, the styles sometimes differed slightly, and sometimes quite seriously. In the process of conversion, the project began to look visually better - small, but noticeable by eye differences on different pages began to gradually fade.

    In the future, we managed to refactor similar controls with completely different styles - we removed rarely used ones, changing them to analogues. If there were too many uses, they took the most successful control from the point of view of CSS and “made up” for it all the others. Not everything was removed or made up, but more on that later.

    Finally, hands reached the library of general controls. As I already wrote, only part of the styles was taken out in the general part. Then these styles already overlapped in each of our web applications. The idea was not very successful - everything that overlapped overlapped in each application in the same way, i.e. it was actually copy-paste. However, not really. When the design needed to change the colors a little, they changed only in one place, forgetting about the rest. We took all this into the general part and then for a long time killed copy-paste. Applications were better - toolbars, grids and other controls of different colors became similar to each other.

    At some point, we realized that it would be nice to put some kind of CSS Reset into this library. Prior to this, CSS Reset was introduced in only two applications, and each in its own. As a result, it was decided to useNormalize.css , which we included at the very beginning. There we added basic styles for our application - font size and typeface (also different everywhere) and many other things.

    At this moment, we were still engaged in one way or another removing copy-paste and unifying styles. Finally, hands reached the custom skins. In fact, they did not change the interface much, but for some reason they contained a huge number of styles. There were about 50 skins, some old ones were handwritten, newer ones used LESS. Despite this, there was no common template, the generation was done manually, and the result was then docked to some common part. We started with them. Firstly, they took out a common pattern (not all matched) and a repeating part. Secondly, we configured the generation of CSS files when building a project using the lessc utility . Then we proceeded to the old skins, where LESS was not used.

    Despite the large amount of code, everything turned out to be not much modified subsequently copy-paste. In the end, we got the same common part for everyone, a 1.5-screen template and 50 less-files with 15 variables for customizing the skin.

    A common piece was laid out at the end of our giant file. Since with equal weights of CSS selectors, priority will be given to what is lower in the text, this is important. At the next stage of refactoring, the add-on to Visual Studio - Web Essentials helped us a lot . The thing is very useful, in short, a kind of CSS Resharper. In addition to finding syntax errors and tips for adding the missing vendor prefix, Web Essentials helps you look for the same classes within a file. And it turned out that in our code such a situation often arose.

    A selector is defined, say on line 6255:
    .topmenu-links
    {
        margin-top: 15px;
        background-color: blue;
    }
    

    Then somewhere on the 13467th:
    .topmenu-links
    {
        margin-top: 10px;
        background-color: green;
    }
    

    I'm exaggerating a little, but it was like that. Moreover, the case was not an isolated, but a massive one. It came to four floors. Web Essentials mercilessly swears at such things, so that they were all found and deleted. As I said, if the weights of the selectors are equal, the priority is lower, so remove the selectors from above and combine them. The process is a bit risky. With a large number of different styles hung on the same element and the scatter of selectors across the file, moving is fraught with a change in priority. But there's nothing to be done. Throughout the work, our QA periodically walked around all the pages of the system and compared the view with production.

    At some point, we were ripe before breaking up our huge CSS into segments. It turned out 120. When building the file was going back to one. And after a while we switched to LESS.

    How is it now


    Let's see how this all looks with an example .

    Let's simplify the task a bit and imagine that we have a library of common controls ( CommonControls) and a project for static content (CDN), which is used in the main web project.

    Screenshot of solution with projects


    In the library with controls, we have LESS files, which when build are collected into one ( common-controls.less) and then translated into CSS ( common-controls.css).

    CommonControls project screenshot


    Let's consider in more detail what is stored where.
    • 01-essentials.lessstores only variables and mixins. They are used by LESS files of the library of controls, as well as files of other projects.
    • 02-normalize.less. I already talked a little about him. It stores CSS normalization code slightly modified for the needs of the project.
    • 03-default-styles.lessstores common design styles (for example, the background color of the element body, the typeface of the font used, etc.)
    • 04-helpers.less stores helper classes, like margins, paddings already described.
    • Next come the actual styles of controls.

    Configure Build Events for the CommonControls project. I put everything in a separate file so as not to edit or merge the project file every time the contents of the script change.

    The build events settings window


    The script code is very simple. We put together all the LESS files in the folder Stylesheetsand transfer the result to the folder CombinedStylesheets. Then we launch the preprocessor and get ready CSS.
    set ProjectDir=%~1
    copy "%ProjectDir%Stylesheets\*.less" %ProjectDir%CombinedStylesheets\common-controls.less
    call "%ProjectDir%..\Tools\lessc\lessc.cmd" %ProjectDir%CombinedStylesheets\common-controls.less %ProjectDir%CombinedStylesheets\common-controls.css
    

    Now let's look at the styles of the Cdn project. The folder _csspartscontains the project styles, which are then assembled into a file combined.less. There are a lot of files in a real project. In the screenshot, everything is a little simplified.

    Cdn project screenshot


    The sequence of files does not matter much, except for the very first and the very last.

    001-imports.lesscontains the following code:
    // Importing LESS template from CommonControls
    @import "../../CommonControls/Stylesheets/01-essentials.less";
    // Usual CSS import
    @import "common-controls.css";
    

    The first directive imports the contents of the LESS file, in this case 01-essentials.less. This is the same as if we concatenated this file with the rest when combining. Import allows you to use all the variables and mixins that we defined in the library CommonControls. The second directive - classic import - is generated as is in the resulting CSS. In general, CSS imports are not recommended, and the only reason it is here is IE9.

    z-ie9-special.lesscontains one single selector, which is the last one in the combined file and is used on a special page to understand whether it is used or not. If the total number of selectors has exceeded 4095, then the style will not apply. So you need to split the file into parts. In fact, we did not have to combine the resulting CSS of the control library and the actual CSS for the web project.

    When building, the following things happen:
    @REM Copy common controls stylesheet
    COPY %ProjectDir%..\CommonControls\CombinedStylesheets\common-controls.css "%ProjectDir%Skins\common-controls.css" /Y
    @REM Combine CDN LESS files and run preprocessor
    copy "%ProjectDir%Skins\_cssparts\*.less" %ProjectDir%Skins\combined.less
    call "%ProjectDir%..\Tools\lessc\lessc.cmd" %ProjectDir%Skins\combined.less %ProjectDir%Skins\combined.css
    

    The Skinscombined CSS library of controls and CSS for the web project gets to the root folder . In real projects, combining the resulting CSS can be made more elegant than concatenating files, but this is just an example.

    Now let's look at the generation of custom skins.

    Screenshot of the folder with custom skins in the Cdn project


    The folder _custom-partscontains a template for generation custom-template.less. Suppose for now it’s enough for us to customize the colors of the headers H1 and H2 (in reality, of course, there are a lot more things). custom-template.lesswill look like this:
    h1
    {
        color: @h1Color;
    }
    h2
    {
        color: @h2Color;
    }
    

    Default-values.less will contain the default values ​​of variables (in order to be able to overlap not everything in the skin in a row, but only some of the values):
    @h1Color: #F58024;
    @h2Color: #E67820;
    

    In each of the skins ( skin.less) there will be something like this code:
    @import "..\_custom-parts\default-values.less";
    @h1Color: #000;
    @h2Color : #707050;
    @import "..\_custom-parts\custom-template.less";
    

    We import the default values, overlap them with our values ​​and import the template.

    To generate all this, write this code in the pre build event:
    @REM Regenerate customskins using their LESS templates
    for /r "%ProjectDir%Skins\" %%i in (*.less) do (
        if "%%~nxi"=="skin.less" call "%ProjectDir%..\Tools\lessc\lessc.cmd" "%%~dpnxi" "%%~dpni.css"
    )
    

    At the output next to each skin.lesswe get, skin.cssfor example, the above of this form:
    h1
    {
      color: #000000;
    }
    h2
    {
      color: #707050;
    }
    

    In general, at first, the contents of our LESS files (not counting custom skins) were no different from ordinary CSS. With a few exceptions, when the parser refused to accept the invalid code, let's say this:
    margin-top: -4px\0/IE8+9;
    

    Not sure if the hack for IE looks that way, but God bless him. In LESS, you can escape a string using characters ~"":
    margin-top: ~"-4px\0/IE8+9";
    

    Everything else went smoothly. Simple variables soon began to appear:
    @сtDefaultFontSize: 14px;
    

    Then mixins are more complicated:
    .hAccessibilityHidden()
    {
        position: absolute;
        left: -10000px;
        top: -4000px;
        overflow: hidden;
        width: 1px;
        height: 1px;
    }
    

    The meaning of this mixin is that in addition to its use in the auxiliary class, it is also used in some others. In our case, it has become even more interesting. When at some point we realized that it was not possible to rewrite, or even “make up” according to the styles of 12 grids, it was a good idea to put common colors and styles into variables and mixins. It turns out that for old projects LESS is even more interesting than for new ones. In general, there is where to roam. For example, the generation of background-image for buttons of various types in the presence of a sprite:
    .ct-button-helper(@index, @name, @buttonHeight: 30, @buttonBorderThickness: 1)
    {
        @className: ~".ct-button-@{name}";
        @offset: (@buttonHeight - 2*@buttonBorderThickness - @buttonIconSize) / 2;
        @positionY: @offset - (@index * (@buttonIconSize + @buttonIconSpacingInSprite));
        @{className} { background-position: 8px unit(@positionY, px); }
        @{className}.ct-button-rightimage { background-position: 100% unit(@positionY, px); }
    }
    

    We call something like this:
    .ct-button-helper (0, "save");
    .ct-button-helper (1, "save[disabled]");
    .ct-button-helper (2, "cancel");
    .ct-button-helper (3, "cancel[disabled]");
    

    It’s still good to generate CSS for font descriptions, although the implementation of the mixin often depends on the particular font.

    Summary


    Let’s take a brief look at what we have done.
    • We cleaned out a huge amount of unused styles.
    • Удалили большое количество классов, использующихся только в одном месте, заменив их на вспомогательные классы. На самом деле это очень значительная часть всего CSS в проекте, не стоит недооценивать этот пункт.
    • Вынесли общую часть из кастомных скинов, добавив её в конец основного CSS. Этот пункт важно было выполнить перед следующим. Использовали LESS для генерации кастомных скинов.
    • Удалили большое количество перекрытий одного и того же класса несколько раз в одном и том же CSS файле. Тут очень помог WebEssentials. Этот пункт тоже важно было выполнить перед следующим.
    • Разбили общий CSS на части для удобства редактирования. При билде все эти части собираются в один файл.
    • Вынесли перекрытия стилей контролов в сами стили контролов. Удалили копипасту в стилях остальных приложений.
    • Поскольку во всех приложениях использовался единый стиль оформления, общие части (normalize.css, размеры шрифтов, цвета фона) тоже вынесли в библиотеку контролов, удалив куски CSS из всех веб-приложений.
    • Перешли на LESS во всех веб-приложениях.
    • Вынесли часть часто используемых вещей в переменные и миксины в библиотеке контролов, подключили их к каждому веб-приложению, так что использовать их можно везде.
    • Сделали обёртку на C# (простой статический класс со статическими пропертями) для часто используемых CSS классов. Не для всех, их очень много, и не всегда есть смысл.
    • Внедрили префиксы… Не во всех местах, а в часто используемых. Стараемся использовать префиксы для всех новых стилей, но всем пофиг.

    In general, we regained control of the code. Subsequent redesign went without a problem. In addition, they made him smarter by changing component by component. Not to say that the code has become perfect or that it has become 10 times smaller. We still have a lot of things, and it can be difficult to figure out. But on the other hand, the number of copy paste decreased significantly and as a result, there were fewer visual differences in different parts of the system, which in theory should look the same.

    Also popular now: