Drupal网站的前端性能优化:第3部分

在第1部分中,我们介绍了Drupal缓存优化,因此我们只要求服务器在绝对必要时生成响应。在第2部分中,我们讨论了Drupal图像优化,以便我们运送到浏览器的资产尽可能轻。如果你还没有读过这些文章,我们建议您从头开始阅读。

我们优化之旅的下一步实际上是促使本系列开始的原因。在我们确保网站尽可能频繁地生成静态缓存的页面,并确保这些页面的所有花哨部分都尽可能小和灵活之后,我们需要确保我们通过网络发送的尽可能多的内容对访问者也有实际用处。

一、只传输访客当前需要的东西

你能做的最快的事情之一就是什么都不做。什么都不会立刻发生。在你思考之前,什么都不会发生。当涉及到软件时,没有代码比没有代码更快。

著名的汽车传奇人物、莲花创始人科林·查普曼常说:“简化,然后增加轻盈感。”

查普曼明白,仅仅携带较小的质量就可以使汽车的速度与更大的发动机一样有效,而且它还提供了许多其他好处,从燃油效率到操控性和耐用性。你可以通过简单地去除完成工作所不需要的东西,在不增加额外复杂性和顾虑的情况下获得更好的结果。

对于网站来说,我们经常携带多余内容的最大部分之一是前端,如CSS和Javascript。

我们应该向网站访问者发送他们消费屏幕上内容所需的最低数据量。当访问主页时,用户不需要在事件页面上使用所有的CSS和Javascript。当访问产品页面时,他们不需要所有的代码来绘制业务位置图。只向访问者提供用于呈现他们当前正在查看的内容的资产,你会让他们的体验更愉快,也会更好地保护环境。

在长风云Drupal开发团队,我们使用Drupal来构建客户端网站,它是构建高性能网络体验的绝佳工具。Drupal的库系统通常是一个未充分利用的网站性能工具。使用它,网站CSS和Javascript只有在提供了需要的内容时才能提供给用户。

二、预处理程序

设置这些工具的最常见方法是将CSS和Javascript聚合为单个的CSS和Javascript文件。

这种配置有一个合理的逻辑。这是最简单的配置,它可以完成任务。变量只在所有SCSS(Sass)文件中工作,而不在文件本身进行显式导入,因为主清单文件很早就包含了变量。您的所有样式都将有适当的前缀,并且您只需要在主题的配置中包括一个库。自身很简洁。

对于一个必须了解网站上所有技术的工作开发人员来说,从PHP到CSS和Javascript,通常没有太多时间来微调前端工具的配置。事情被配置为接近默认值,接近产生每个人都在寻找的最小可行构建的设置。这个网站看起来不错,每个接触这个项目的人都可以写一些风格,重新加载页面,看看他们做了什么。

直到最近,大多数Drupal基本主题(至少我使用过),甚至那些包含组件驱动的开发和设计的主题,都是开箱即用的,被配置为生成一个完整的CSS和Javascript文件。在过去的几年里,情况发生了变化。Emulsify(成都肠粉云Drupal开发团队在项目开发中大量使用)在2022年发布的4.2.0版本中开始拆分其组件CSS。基于Bootstrap的入门主题Radix在2022年也做出了类似的改变。其他主题,如备受尊敬的Bootstrap主题,已经开始提供分裂的Javascript文件,但仍然提供一个单一的CSS文件。

这些工具帮助我们在设计阶段学会以模块化的方式思考我们的设计和主题,但我们多年来一直将这些精心策划的主题组件编译成一个大的CSS文件,发送给每个页面上的每个用户。


有一段时间,在可能的情况下运送单个文件是有技术原因的。在HTTP/1时代,有一种减少资产数量的说法,因为不可能多路传输连接并并行下载资产。每一个连接都需要时间来建立,并且它会阻止随后的所有连接。由于HTTP/2在现代浏览器中被广泛采用,这不再是一个问题。

当时,我们解决的问题是主题开发的成本和挫折感,而不是页面速度成本和挫败感。我们关注的是将高质量、视觉震撼的设计运送到运转时间最少的网站。对于致力于交付复杂项目的Drupal开发人员来说,这些实践的采用是非常有效的。

三、现代前端框架和单目录组件

大约在Drupal社区逐渐了解组件的同时,现代史上最流行的Javascript框架之一React出现了。React之后不久,我最喜欢的全胖框架VueJS也以它的存在为世界增光添彩。我不能说开发人员最初使用React的经验,但当我开始使用VueJS时,我立即被单文件组件的概念所吸引。一个.vue文件提供了使组件工作所需的所有标记、Javascript和CSS(或SCSS)。加上易于使用的选项API,它使构建复杂且可重复使用的组件成为一种乐趣。

这种范式开始进入标准的Drupal主题化。像Emulsify这样的项目被设置为将CSS、模板和Javascript放在一个文件夹中,从而提供相同的概念优势。

Drupal组件模块,以及新的单目录组件核心模块几乎达成了协议,我们不仅应该在原子组件中进行设计,而且应该以同样的方式将它们运送给最终用户。

单目录组件最近从实验状态升级为稳定状态,升级到Drupal 10.3,但Drupal核心今天支持手动拆分CSS和Javascript库,并且在相当长的一段时间内:

将你的CSS编译成单独的文件(或者一开始只是在单独的文件中编写普通的CSS)。

将这些文件作为条目添加到主题/模块库.yml文件中

只有在需要时才将这些库(通过各种方法)附加到页面上。

根据主题的配置方式,这个三步过程将包含一些相关的子步骤。

四、编译以分离文件

这可能是最困难的部分,这取决于您对主题绑定器的API的了解程度(如果您正在绑定)。在Gulp中可能看起来像这样:

Old:

 // compile CSS
gulp.task('css-compile', function () {
  return gulp.src(['scss/style.scss'])
    .pipe(sass().on('error', sass.logError))
    .pipe(concat('app.min.css'))
    .pipe(postcss([autoprefixer()]))
    .pipe(cleanCSS({compatibility: 'ie9'}))
    .pipe(gulp.dest('css'))
    .pipe(browserSync.stream());
});

New:

// compile CSS
function globalCss() {
  return src('scss/global.scss')
    .pipe(sassGlob())
    .pipe(sass().on('error', sass.logError))
    .pipe(postcss([autoprefixer()]))
    .pipe(cleanCSS({compatibility: 'ie9'}))
    .pipe(dest('css'))
    .pipe(browserSync.stream());
}
function componentCss() {
  return src('scss/components/**/*.scss')
    .pipe(sassGlob())
    .pipe(sass().on('error', sass.logError))
    .pipe(postcss([autoprefixer()]))
    .pipe(cleanCSS({compatibility: 'ie9'}))
    .pipe(dest('css'))
    .pipe(browserSync.stream());
}

这是我们最近为提高网站的性能而进行的重构的一个例子。

这种重构也从较旧的gullow任务编写风格转变为较新的风格,但您可以主要关注函数内部的内容,而不是函数声明的风格。在这里,我们已经从一个单一的CSS文件转移到一个拆分的文件集,其中有一个低级别的全局文件,其中包括随处使用的常见样式(字体声明、基本页面元素样式和网格框架)。

使用Emulsify/Webpack的示例如下所示:

Old:

// css.js -  this file is a single manifest with ALL of our sass partials imported.
import '../components/style.scss';
// webpack.common.js
function getEntries(pattern) {
  const entries = {};
  // We were on to something with the Javascript...
  glob.sync(pattern).forEach((file) => {
    const filePath = file.split('components/')[1];
    const newfilePath = `js/${filePath.replace('.js', '')}`;
    entries[newfilePath] = file;
  });
  // One monolithic but beautiful CSS file...
  entries.css = path.resolve(webpackDir, 'css.js');
  return entries;
}

New: 

// Glob pattern for scss files that ignore file names prefixed with underscore.
const scssPattern = path.resolve(rootDir, 'components/**/!(_*).scss');
// Glob pattern for JS files.
const jsPattern = path.resolve(
  rootDir,
  'components/**/!(*.stories|*.component|*.min|*.test).js',
);
// Prepare list of scss and js files for "entry".
function getEntries(scssPattern, jsPattern) {
  const entries = {};
  // SCSS entries - THIS IS THE GOODNESS!!!!
  glob.sync(scssPattern).forEach((file) => {
    const filePath = file.split('components/')[1];
    const newfilePath = `css/${filePath.replace('.scss', '')}`;
    entries[newfilePath] = file;
  });
  // JS entries - sameish as before
  glob.sync(jsPattern).forEach((file) => {
    const filePath = file.split('components/')[1];
    const newfilePath = `js/${filePath.replace('.js', '')}`;
    entries[newfilePath] = file;
  });
  // Global CSS file for things needed everywhere.
  entries.style = path.resolve(webpackDir, 'css.js');
  return entries;
}

具体细节将取决于主题是如何构建的,但总体思路是在组件SCSS文件上循环,并为每个文件吐出一个互补的单独CSS文件。您可能还需要重构各个文件,以显式导入所使用的变量或辅助函数。您的bundler/编译器通常会在出现问题时告诉您。

设置库文件

现在您已经拥有了所有这些独立的CSS文件,现在是时候跳到theme_name.libraries.yml文件中,将它们全部声明为库了。变化看起来有点像这样:

以前:
theme_name:
  version: 1.0
  css:
    theme:
      css/app.min.css: { minified: true}
...


New:

global-styling:
  version: 1.x
  css:
    base:
      css/global.css: { minified: true}
...
# components
accordion:
  version: 1.x
  css:
    component:
      css/accordion.css: { minified: true }
alert:
  version: 1.x
  css:
    component:
      css/alert.css: { minified: true }
alert_banner:
  version: 1.x
  css:
    component:
      css/alert_banner.css: { minified: true }
  dependencies:
    - theme_name/alert
...

为了简洁起见,我省略了Javascript。您可以看到的是组件库的添加。

一旦库被拆分,最后一步是在需要时将这些库附加到网站上,这可以通过多种方式实现,最简单的是将其附加到需要样式和脚本的trick模板上。以下是alert.twig组件的示例:

{{ attach_library('theme_name/alert') }}
<div{{attributes}}>
  {{ content }}
</div>

这也可以通过Drupal的“库覆盖”和“库扩展”功能,通过预处理钩子或任何其他允许修改渲染数组的Drupal钩子来实现:

/**
 * Implements hook_form_alter().
 */
function my_theme_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $form['#attached']['library'][] = 'my_theme/forms';
}

有了这些,你只会在访问者需要的页面上向他们发送CSS和Javascript。

五、Frameworks and PurgeCSS

一旦您为主题组件拆分了CSS和Javascript,您将在减少未使用的CSS占用方面取得重大进展。然而,通常仍会有更多的全局CSS。样式,如基本元素样式(h1s、p标记等),以及字体定义,也许最重要的是,网格样式和实用程序类。

大多数基本元素样式和字体定义都不会被视为未使用的样式(如果配置正确的话),但在任何面向框架的代码中,尤其是网格和实用程序类中,都可能会遇到很大一部分未使用的风格。

这些工具提供了可预测的选项,供您应用于网站构建,如容器、排水沟、柱和间距实用程序。它们提供了标准化和可预测性,并可以显著降低网站CSS的复杂性,但这是有代价的。

大多数网站不会完全使用网格或实用程序框架的每一个选项。如果你不采取任何措施,你可能会在每次页面加载时发送数百KB未使用的CSS。这个问题的一个很好的解决方案是PurgeCSS工具。

配置PurgeCSS相当简单,但需要注意以下几点:

purgecss({
      content: [
        'templates/**/*.twig',
        '../../../modules/custom/**/templates/**/*.twig'
      ],
      safelist: [/^icon-logged.*/, /.*block-postsociallinks.*/, /.*data-extlink.*/, 'figure']
})

“内容”映射为PurgeCSS提供了一组位置,用于查找您的网站模板,其中应包含您的主题将使用的所有CSS选择器。“safelist”数组是作为CSS的转义填充提供的,您需要确保它不会被清除,这可能不会显示在您的模板中(至少不会以传统方式)。这可能是因为您的CSS针对的是另一个模块、核心或主题模板中没有提供的任何其他模块提供的内容。您可以在主题中动态构建PurgeCSS无法解析的类名,也可以在PHP函数中定义类/标记。

您将遇到的最大问题是需要清除的样式。使用内容定义和安全列表可以毫不费力地解决大多数问题,但也可以通过仅对可能包含框架代码的库运行PurgeCSS来避免一些复杂问题。通常不需要清除组件CSS文件中编写的样式,因为这些文件通常只包含以组件中的标记为激光目标的CSS。

应用所有这些后,您现在应该只运送访问者所需的资产,从而节省大量的时间、CPU周期和碳,这些都会随着网站的流量而变化。

回顾一下,我们现在希望尽可能发送静态缓存的页面。我们为游客提供的美丽图像重量轻,经过调整以提供正确的像素,我们熟练地提供游客此刻所需的CSS和Javascript,仅此而已。如果我们止步于此,我们的网站很可能已经提供了出色的核心网络重要指标。我们将节省大量的数据传输,并将大量宝贵的时间回馈给我们网站的访问者。

下周将是本系列的最后一篇文章,我们将讨论更复杂的问题,即当我们没有选项时,我们该怎么办?当我们必须交付的资产仍在减缓页面速度时,我们该怎么办?我们仍然有一些技巧和窍门来处理这些情况,这些技巧和窍门将帮助您为网络公民提供更快的页面,我们希望在那里见到您。

六、专业的Drupal开发团队 - 成都长风云Drupal开发团队

在2024年,我们已经专注于Drupal开发16年(始于2008年)。无论您计划从Drupal7升级到Drupal10、从旧版本迁移到Drupal9、基于Drupal开发新的系统、企业官网、电商网站,维护基于Drupal开发的系统等,我们都能依靠我们的专业技术为您完成。免费咨询Drupal升级方案,手机号:13795726015 或 微信号:changfengqj 扫微信二维码:

联系我们

提供基于Drupal的门户网站、电子商务网站、移动应用开发及托管服务

联系电话
137-9572-6015
长按加微信
长风云微信
长按关注公众号
长风云公众号