WordPress 加载速度优化实践

最近兴起开坑了一款新主题,名为 Pure,寓意简洁轻量,目前还处于自己用着玩的阶段,等调教好了自然会拉出来接客见客。作为本款主题案例展示站,也要契合主题风格,自然对访问速度有要求。之前有提到博客换到到了独立小开间,不再挤大通铺了,访问速度略有提升,索性在别的地方也一并优化。经过几日折腾,取得了不错的阶段性成果,现将折腾过程记录总结如下:

优化思路以用户在浏览器输入网址到页面加载完毕为线索(不禁想起了前端经典面试题从输入URL到页面加载发生了什么😂)。

以新访客访问为场景,其大致分为:

  1. 用户输入 URL,浏览器解析 URL 发送请求
  2. DNS 查找服务器地址
  3. 浏览器发送 HTTP 请求
  4. 服务器相应请求并返回 HTTP 报文
  5. 浏览器解析渲染页面

作为 Web Master 请关注重点可优化的点为加重项

另外优化效果如何量化呢,当然是借助浏览器的开发者工具。我自己习惯使用 Chrome DevTools,需要关注是 Network 面板,如何使用 Network 分析网站性能,官方的使用手册在这里,最为前置技能大家请预先了解下。

打开 Chrome DevTools 的 Network 面板找到 https://bluest.xyz 这个请求点开,查看 timing 选项卡,这里包含了资源加载的所有时间,具体每个条目具体代表含义请预习了解 Resource Timing,此外此文精髓就是下图,信息的标注了浏览器请求生命周期的主要阶段,方便大家查阅我贴在下面。

DNS Lockup

如果用 Chrome 你开启了访客模式访问 Godaddy 并观察首页加载状况的 Timing 面板,会发现 DNS Lookup 项时长有几十 ms,该时长也是 DNS 查询占用的时间。什么是 DNS 呢:

域名系统(英文:Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS 使用 TCP 和 UDP 端口 53。当前,对于每一级域名长度的限制是 63 个字符,域名总长度则不能超过 253 个字符。
via wikipedia

直白的来说 DNS 是用于查询服务器 IP 与 域名之间的映射的,DNS 查询的速度决定了从客户端寻找到你服务器的速度。假如你的服务器的域名解析服务器在国外,比如我之前用的 Godaddy 默认的 DNS 解析服务,那么这段解析时长就会比较耗时。

DNSPod

DNS 查询服务作为互联的基础设施,是一项很烧钱的项目,需要投入大量的基础设施与人力成本,这也是我们这些个人博客小站长承受不起的。不过我们有捷径可走:就是使用国内现成的 DNS 解析服务,他们针对国内的网络环境线路做了优化,相较于使用国外的一些 DNS 解析服务还是有很大的速度优势的。特别是 DNSPod 这样的良心域名解析服务商还提供了针对中小站长的免费域名解析服务。当然推介 DNSPod 只是个人选择,用了好多年一直很稳定,国内也不乏其他类似的优秀 DNS 解析服务提供商,大家也可自行选用。

设置 TTL

浏览器做 DNS 解析也不会傻乎乎的每次都做递归查询的,浏览器、操作系统、路由器、ISP、根域名服务器、 顶级域名服务器都会利用 DNS 缓存来优化查询速度。既然说到缓存必然说到失效时间,那么 TTL 就是用来控制缓存失效时长的。

TTL(Time To Live),就是一条域名解析记录在 DNS 服务器中的存留时间。

一般情况下,域名的各种记录是极少更改的,很可能几个月、几年内都不会有什么变化,所以我们可以把 TTL 时长设置长一些,这样能让各个环节对 DNS 解析缓存时长延长,减少查询的时长。

DNS prefetching

通常我们页面上会引入多个域名,开启 DNS Prefetching 可使浏览器主动去提前执行域名解析,避免对这些域名进行 DNS 解析产生的高延迟。

针对特定域名开启 DNS Prefetching 的方式如下:

<link rel="dns-prefetch" href="//host_name_to_prefetch.com">

其实浏览器会对 a 标签的 href 自动启用 DNS Prefetching,所以 a 标签里包含的域名不需要在 head 中手动设置 link。但是在 HTTPS 下面不起作用,需要 <meta> 中强制开启该功能。强制开启方式可以通过在服务器端发送 X-DNS-Prefetch-Control 报头,或是在文档中使用值为 http-equiv<meta> 标签打开 DNS 预读取:

<meta http-equiv="x-dns-prefetch-control" content="on">

更多内容请查看 Chromium 上关于 DNS Prefetching 的详细介绍。

域名收敛

移动端由于延迟与查询链路更长的关系,DNS 解析会比桌面端慢得多。移动端在增加域的同时,往往会给浏览器带来 DNS 解析的开销,所以域名收敛这一优化策略被提出了:即减少域名的数量。一般来说在移动端,域名分散的数量最好在 3 个以下。

不过针对 PC 端采取的优化策略就有变化了——提倡域名发散等,这个下面会解释。理论上 DNS 查询时间是跟域名数量是成正比的,只不过引入了其他影响页面加载速度的变量,最终策略的不同是取舍不同罢了。

HTTP 请求优化

说完了 DNS Lookup 时长的优化,这时浏览器得到了服务器的 IP 地址,接下来就轮到浏览器于服务器之间通信的阶段了,两者之间通信走的是 HTTP 协议,于是下面我们要做得就是针对 HTTP 请求做优化了。

域名发散

首先我们需要了解到有这么一个浏览器行为,浏览器会限制一个域名下的并发请求资源数,具体不同的游览器有不同的限制数量。至于浏览器这么做的原因可以参考浏览器允许的并发请求资源数是什么意思?

也就是假如一个页面上有同一域下的 12 个静态资源,而浏览器对同一域名下并发请求资源数是 6 个,那么在页面加载开始时这些静态资源中至少有 6 个处于被阻塞的状态。如果我们将这被阻塞的静态资源分配到另一个域名下,这样想当于启用了双车道,就缓解了资源被阻塞的问题,这种做法就是域名发散。这虽然增加了一点 NDS 查询与 HTPP 握手的开销等,但在总体上是利大于弊的。

如果你的资源数量还没有膨胀到很大的话,那么域名是越收敛越好,这主要涉及到请求支援优化了,别急着个下文再说。

升级到 HTTP 2.0

升级 HTTP 2.0 这项优化实际上我是没有进行下去的,因为下文提到优化项之一 CDN。目前浏览器不支持非 SSL 加密的 HTTP 2.0 请求,也就是说想用上 HTTP 2.0 必须先上 HTTPS,但是我所使用的 CDN 使用 HTTPS 需要备案,由于博客尚未备案所以被搁浅了。

由于经受不住访问速度的诱惑,还是更换到了腾讯云、切换到 https://bluest.xyz 这个可备案域名、另外倒腾备案,倒腾 SSL 加密,好不折腾。

上文的域名发散的措施是针对 HTTP/1.0 及 HTTP/1.1 时代做的,HTTP/2.0 的玩法就变了。简单来说 HTTP/2.0 的优势有如下:

  1. 多路复用
  2. 头部压缩
  3. 服务器推送

具体优势分析与实践在之前在文章《LAMP 启用 HTTP/2 一波三折》中有人言亦言的解释,这里就不再赘述了。需要说明的是如果在 HTTP/2.0 下域名发散策略就需要重新考虑了,因为多路复用使得该策略不再有必要。


说完了 HTTP 的时间,就该来到 requestresponse 时间的优化环节了了。request 我这里没有太多的可说的了,除了使用钞能力让你的服务器拥有更好的访问线路外,还与客户端上传带宽等其他因素有关。接下来我们就要对 response 的速度进行优化了,同样服务器的线路速度、宽带大小这些因素也会显著影响其速度,当然这些用钞能力就能解决的常见问题,这里也不再纠结了。

TTFB

通常来说,每次服务器收到请求(这里以我博客的 LAMP 架构为例),都要经历这些过程(由于个人不是专业的 PHP 后端,这里叙述可能不太严谨,请见谅):

  1. Apache 服务器接受并处理请求
  2. 执行 PHP 代码逻辑
  3. 执行 MySQL 查询
  4. 最终生成 HTML 代码返回给客户端

简而言之,这个过程的速度取决于你的服务器够不够快!当然这个速度也是

TTFB (Time To First Byte),是最初的网络请求被发起到从服务器接收到第一个字节这段时间,它包含了 TCP 连接时间,发送 HTTP 请求时间和获得响应消息第一个字节的时间。
via wikipedia

TTFB 包括了浏览器发送请求到服务器相应请求并返回数据的时间。换句话说,除了用户与服务器之间的网络速度,影响 TTFB 的因素就是服务器处理请求的速度了。

WP Super Cache

WP Super Cache

Memcached

Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态、数据库驱动网站的速度。Memcached 基于一个存储键/值对的 hashmap。其守护进程(daemon )是用 C 写的,但是客户端可以用任何语言来编写,并通过 memcached 协议与守护进程通信。
via 百度百科

同样是基于缓存,WP Super Cache 的缓存手段是将文件静态化以文件的形式缓存在磁盘上,Memcached 则是基于内存缓存,而磁盘读写 I/O 速度是远远比不上内存读写速度的。

# 安装 memcached
apt-get install memcached
# memcached php 拓展****
apt-get install php-memcached
# 重启 apache 使之生效
service apache2 restart

使用如下代码测试,如果输出 100 则表示安装成功:

<?php
$m = new Memcached();
$m->addServer( '127.0.0.1', 11211 );
$m->set( 'foo', 100 );
echo $m->get( 'foo' ) . "\n";

https://github.com/Automattic/wp-memcached/blob/master/object-cache.php

MySql 缓存

说完了服务器边的优化,这里说

关键渲染路径优化

关键渲染路径优化

关键渲染路径优化

Apache 服务器设置

gzip

gzip

apache:http://httpd.apache.org/docs/current/mod/mod_deflate.html
https://checkgzipcompression.com/

善用缓存

casche-control

CSS

JavaScript

资源加载

静态资源优化

压缩合并

我们需要先认识到一个大前提:浏览器在同一时间内,会限制同一个域的同时请求数,尽管这个数量一再提高,现基本以提升至 6 个以上。假设浏览器限制请求数目为 4 个,页面需要加载 8 个静态资源,其他 4 个静态资源就处于被阻塞状态,直到之前的某一个请求完成。

  1. 较少请求 head 头

CDN

CDN的全称是 Content Delivery Network,即内容分发网络。 其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。
via

还是那句话,专业的事情最好交给专业的人来做。由于 CDN 专注于

  1. CDN 速度快
  2. CDN 域名发散
  3. 通过设置 Cookies,CDN 请求可不带 Cookies 减小了请求头

由于 WP Super Cache 的支持,配置 CDN 变得非常简单。

前端公共 CDN 库

前端公共 CDN 库是将常用的前端资源存放在 CDN 上,方便开发者调用。上文列举的

  1. jQuery
  2. lazyLoad
  3. 公共 CDN 库

图片优化

图片压缩

  1. TinyPNG
  2. TinyJPG

图片 LazyLoad

特色社会主义前端优化

WordPress 这个外来和尚虽然念经念得不错,但是遇到社会主义的墙,还是遇上了

Gavatar 本地缓存

WordPress 的头像服务是依赖 Gavatar,其服务曾在天朝曾有抽风的现象,另外其服务器在国外访问速度也不够快。

我这里做法是直接将 Gavatar 的头像保存在本地,配合 WP Super Cache 也能使用 CDN 加速,为了解决 CDN 缓存问题我加了个时间戳。

/**
 * [my_avatar 将 Gavatar 的头像存储在本地,防止伟大的 GFW Fuck Gavatar,反强奸(很不幸已经被墙了)]
 *
 * @param    string $avatar []
 * @param    mixed $id_or_email [id or email]
 * @param    string $size [头像大小]
 * @param    string $default [默认头像地址]
 * @param    boolean/string $alt    [alt文本]
 *
 * @return string    [html img 字符串]
 */
function my_avatar( $avatar, $id_or_email, $size = '96', $default = '', $alt = false ) {
    $email = '';

    if ( is_numeric( $id_or_email ) ) {
        $id   = (int) $id_or_email;
        $user = get_userdata( $id );

        if ( $user ) {
            $email = $user->user_email;
        }
    } elseif ( is_object( $id_or_email ) ) {
        $allowed_comment_types = apply_filters( 'get_avatar_comment_types', array( 'comment' ) );

        if ( ! empty( $id_or_email->comment_type ) && ! in_array( $id_or_email->comment_type, (array) $allowed_comment_types ) ) {
            return false;
        }

        if ( ! empty( $id_or_email->user_id ) ) {
            $id   = (int) $id_or_email->user_id;
            $user = get_userdata( $id );
            if ( $user ) {
                $email = $user->user_email;
            }
        }

        if ( ! $email && ! empty( $id_or_email->comment_author_email ) ) {
            $email = $id_or_email->comment_author_email;
        }
    } else {
        $email = $id_or_email;
    }


    $FOLDER           = '/avatar/';
    $email_md5        = md5( strtolower( trim( $email ) ) );// 对email 进行 md5处理
    $avatar_file_name = $email_md5 . "_" . $size . '.jpg';
    $STORE_PATH       = ABSPATH . $FOLDER; //默认存储地址
    $alt              = ( false === $alt ) ? '' : esc_attr( $alt );
    $avatar_url       = home_url() . $FOLDER . $avatar_file_name; // 猜测在在博客的头像
    $avatar_local     = ABSPATH . $FOLDER . $avatar_file_name;// 猜测本地绝对路径
    $expire           = 604800; //设定7天, 单位:秒
    $r                = get_option( 'avatar_rating' );
    $max_size         = 10240000;
    // 默认的头像 在add_filter get_avatar 会默认传入默认的url;
    $fix_default = get_stylesheet_directory_uri() . '/assets/image/default_avatar.jpg';

    // 暂时判断目录存在,如果不存在创建,存放的文件夹
    if ( ! is_dir( $STORE_PATH ) ) {
        if ( ! ! mkdir( $STORE_PATH ) ) {
            return null;
        }
    }

    // 判断在本地的头像文件 是否存在或者已经过期
    if ( ! file_exists( $avatar_local ) || ( time() - filemtime( $avatar_local ) ) > $expire ) {

        // 如果不能存在 Gavatar 会返回你设置的地址的头像
        $gavatar_uri = "https://secure.gravatar.com/avatar/" . $email_md5 . '?s=' . $size . '&r=' . $r;

        $response_code = get_http_response_code( $gavatar_uri );

        if ( (integer) $response_code != 200 ) {
            $gavatar_uri = $fix_default;
        }

        @copy( $gavatar_uri, $avatar_local );

        // 如果头像大于 10 MB 那么还用默认头像替代
        if ( filesize( $avatar_local ) > $max_size ) {
            @copy( $fix_default, $avatar_local );
        }
    }

    // 增加时间戳 强制 CDN 正确的回源
    $file_make_time = filemtime( $avatar_local );

    $avatar = "<img title='{$alt}' 
                    alt='{$alt}' src='{$avatar_url}?&t={$file_make_time}' 
                    class='avatar avatar-{$size} photo' height='{$size}' width='{$size}' />";

    return $avatar;
}

/**
 * 获取 HTTP 相应头
 * @param $theURL
 * 
 * @return bool|string
 */
function get_http_response_code( $theURL ) {
    $headers = get_headers( $theURL );
    return substr( $headers[0], 9, 3 );
}

// 替换原来的系统函数
add_filter( 'get_avatar', 'my_avatar', 10, 5 );

上述的方案其实存在一个问题,如果服务端

Page Speed mod

Google mod-pagespeed

钞能力

优化结果

至此页面加载速度性能优化就此告一段落。经过自测,初次访问页面加载速度在 1s 内,如有缓存的情况下甚至可达到 0.5s,测试结果有波动,但基本可以秒开。不过后来我又手欠加了 Google Analytics 与百度统计,还有 Adsense,现在访问速度劣化到 2s 左右了,心塞!!!

持续迭代

性能监控

__

参考

https://github.com/chokcoco/cnblogsArticle/issues/1

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注