I once sat on a mountain and deeply contemplated the mysteries of Drupal development. Actually, I live on a mountain, so I do this every day, and the title of this post isn't a Zen revelation, I stole that from Elon Musk.
I'm not trying to build rockets and send humans to Mars, and I don't want to draw too many parallels between what I do and the complexity of that enterprise, but we do solve some complex problems of critical importance to our clients. Every efficiency we can gain improves outcomes.
The Challenge: Theming Efficiently
When I work on a project, or a problem, I try to distil it down to it's most important elements, and ruthlessly eliminate that which is not efficient, or providing real value. As we trend more towards Javascript front ends and I've had more opportunity to get deeply back into that side of Drupal development, I find myself being more critical of inefficiency, over-engineering, bloat and unnecessary "features". I believe the Developer Experience (DX) is as important as the User Experience (UX). Happy, efficient devs, make quality software.
To that end, I want to simplify and reduce the friction in my workflow. My particular goal for this post is to make a theme cleaner, easier to build and maintain, and perhaps get even better performance.
I'm afraid this might be controversial for some reason, but I've dropped the compiler and task runner from my themes. Every theme I've encountered since I can't remember when, has used SCSS and an often unnecessary JS Compiler. Why run simple jQuery based behaviors through a compiler?
There was a time when I was excited about this approach and could see the benefits of SCSS. Today, I'm not so sure it's necessary. As for Javascript, if you're just doing simple Drupal behaviors, and you're not into Typescript or something, you don't need a compiler for that. Why not avoid the complexity and drop it all from the theme? Of course, decoupled front ends or other advanced Javascript requirements is a different story.
What was great about SASS/SCSS, but maybe isn't so much any more?
- It's CSS Syntax friendly so you already know how to get started with it.
- CSS is CSS Syntax friendly too. :p
- Variables
- CSS has that now, and it's "catching up" in other ways.
- Nested Syntax
- I often see this causing bloat and hard to read code. While they say it's "easier to read/write", I'd say this is only true when there's a disciplined hand crafting well structured SCSS. This is true for CSS as well, and I don't see the advantage of nested syntax.
- Mixins
- Component driven Atomic Design reduces the value of this, and new features of CSS reduce it even further.
- Modularity via @import compiling down to a single file.
- Again I'll mention Component based Atomic Design, as well as Drupal's Aggregation and libraries.yml.
- Popularity and a large SASS Community
- Literally every front end dev uses CSS. ;)
note: 6 of the 7 benefits of SASS over CSS from this 2018 post.
Another thing we got from the compiler was code linting which can be done at some or all of three other points in the development/delivery pipeline; in the editor, in a git pre-commit hook or in automated test runs. There are even CSS linters written in php we can include in the composer.json we're using anyway.
Now that all that's out of the way...
How I Built My Last Project
- Atomic Design
- Components
- Drupal libraries
- Drupal CSS/JS Aggregation
Every front end Drupal developer is probably fully aware of these things. We're doing component based front ends already. I won't belabor each of these points. I'll just share a few thoughts about why I think this is often enough to make fast, reliable and robust front ends, and eliminate some cruft from the process.
Atomic Design
Breaking our user interfaces down into clearly defined patterns gives us consistency, re-usability and a clear hierarchy of components. I kind of assume everyone has encountered this, or some similar methodology, but if not, I highly recommend this book.
Identifying these clear and reusable patterns in our UI allows us to build each one out as a discrete and self contained component that can be pieced together to make more complex features on a page.
Components
Building these Atomic patterns into usable templates creates our components. The self contained components include the twig template and the CSS specific to that component. If there's some discrete JS functionality that goes along with it, it can be include here as well.
- my_theme
- components
- hero
- paragraphs--hero.html.twig
- hero.css
- card
- paragraphs--card.html.twig
- card.css
- ...
We can install the components contrib module to allow our theme templates to be discovered somewhere other than the /templates
directory.
My preference for this project was to use the paragraphs module to create "stackable" components for node content, but you can create or use components in node templates, for blocks, header, nav, etc.
You might even abstract your components out to something more like a pattern library, allowing you to include them in different drupal templates, i.e., in my paragraph--hero.html.twig
file:
{% include "@my-pattern-library/hero/hero.twig" with { ...
In my case I have a set of components I reuse across multiple projects which are defined in a custom module. The component module allows me to access them as components in this way from my theme templates.
In the theme or module's info.yml
file:
component-libraries:
my-components:
paths:
- components
Makes the components in this project's components
path available to include with @my-components/...
.
You can use this with something like Pattern Lab too, and seamlessly integrate components from another resource. If your project is at a level where it needs something like that, then keeping the compilation tooling in a separate pattern library project process still allows us to run without it in the Drupal theme.
Drupal Libraries
Now in our component template files, we can directly attach the library we want associated with the component.
/* Optionally include the custom module's default component styles. */
{{ attach_library('my_component_module/hero') }}
/* Optionally include this theme's component styles, if any. */
{{ attach_library('my_theme/hero') }}
{% include "@my_component_module/hero/hero.twig" with {
'image': content.field_background_image,
'heading': content.field_heading,
'text': content.field_text,
'link': content.field_link
}%}
These libraries are, of course, defined in their respective module's or theme's libraries.yml
file.
By attaching them to the component's templates we allow Drupal to only load them when that particular component is used on a page, instead of one huge compiled CSS or JS file for everything.
Drupal CSS/JS Aggregation
Now when we turn on Aggregation, any libraries loaded with components are pulled together by Drupal and cached as needed. Each page's aggregated CSS is only the size it needs to be to deliver it's components.
Using the Advanced CSS/JS Aggregation contrib module can give us additional improvements, including minifiy which is something we would actually lose without a processor in the development pipeline.
Back to My Mountain Meditation
The component approach gives us an innate modularity to our CSS which vastly improves maintainability and suppresses the opportunity for bloat. The clear structure makes the CSS understandable.
Removing compilation means we can forget maintaining another package list, and github telling us we have vulnerabilities in our package.json all the time. We don't need watchers and browser sync and we can avoid any debate on whether to include compiled assets in the working repo! (hint...you should not.) ;)
We've also just sped up our CI build/test and removed more than one potential point of failure in that process.
I'll be pushing and testing the limits of performance, scalability and maintainability of this project as I prepare it for the wild and help it grow. If I find I've made a terrible mistake, I'll be sure to report back here. But right now, it feels like spring cleaning on the mountain.
"Code like nobody is watching... but then refactor. ;)"