性能,是Web开发中永恒的话题。

一个性能好的网站,加载速度快,用户体验好,搜索引擎排名高,服务器成本低;一个性能差的网站,加载速度慢,用户流失,搜索引擎排名低,服务器成本高。性能,直接影响着网站的成败。

PHP,作为Web开发最流行的语言之一,因为其解释执行、动态类型、灵活方便的特点,性能一直是被诟病的地方。但实际上,只要掌握了正确的优化方法,PHP也能写出高性能的代码。

我自己,做了多年PHP开发,在性能优化方面,积累了一些经验。从最初的只会写功能,到后来关注性能,再到现在能够系统地进行性能优化,一路走来,踩了不少坑,也总结了不少方法。

今天,分享PHP性能优化,从代码层面、数据库层面、服务器层面、缓存层面,全方位讲解PHP性能优化的方法和技巧,帮你让你的代码飞起来。

性能优化的原则

在讲具体的优化方法之前,先讲几个性能优化的基本原则。

1. 不要过早优化

"过早优化是万恶之源。"这是计算机科学家高德纳(Donald Knuth)的名言。

在项目初期,不要过度关注性能,而应该关注功能的实现和代码的可维护性。过早优化,会增加代码的复杂度,降低开发效率,而且很多优化,在项目初期可能是不必要的。

正确的做法是:先实现功能,保证代码的可读性和可维护性,然后在需要的时候,再进行性能优化。

2. 先测量,再优化

性能优化,不能靠猜测,而要靠数据。

在优化之前,先用工具测量,找到性能瓶颈在哪里。是代码慢?还是数据库慢?还是网络慢?还是服务器配置问题?

只有找到了真正的瓶颈,才能有针对性地优化,事半功倍。如果靠猜测优化,可能会优化了不该优化的地方,浪费时间和精力,甚至可能引入新的问题。

常用的PHP性能测量工具:

  • Xdebug:PHP调试和性能分析工具,可以生成函数调用的性能报告
  • Blackfire:PHP性能分析工具,由SensioLabs开发,功能强大
  • New Relic:应用性能监控工具,可以监控PHP应用的性能
  • Apache Bench(ab):HTTP压力测试工具,可以测试网站的并发性能
  • wrk:更先进的HTTP压力测试工具

3. 优化瓶颈,而不是全部

性能优化,要遵循"二八定律":80%的性能问题,来自20%的代码。

所以,优化的时候,要聚焦于那20%的瓶颈代码,而不是全部代码。优化瓶颈,能以最小的成本,获得最大的性能提升。

比如,如果一个网站,80%的时间都花在数据库查询上,那么优化数据库查询,就能获得最大的性能提升;而如果去优化那些只占5%时间的字符串处理,效果就很有限。

4. 权衡优化的成本和收益

性能优化,是有成本的。有些优化,需要修改大量代码,增加复杂度,但性能提升有限;有些优化,只需要简单修改,就能获得显著的性能提升。

所以,优化的时候,要权衡成本和收益。优先做那些成本低、收益高的优化;对于成本高、收益低的优化,可以考虑不做,或者延后做。

5. 优化后要验证

优化完成后,要再次测量,验证优化是否有效,是否引入了新的问题。

有些优化,可能在测试环境有效,但在生产环境无效;有些优化,可能提升了某方面的性能,但降低了另一方面的性能;有些优化,可能引入了新的bug。

所以,优化后,一定要验证,确保优化是有效的、安全的。

代码层面优化

1. 使用最新的PHP版本

PHP的每个新版本,都会有性能提升。

  • PHP 5.x到PHP 7.0:性能提升了约2倍
  • PHP 7.0到PHP 7.1/7.2/7.3/7.4:每个版本都有小幅性能提升
  • PHP 7.4到PHP 8.0:JIT编译器,性能进一步提升

所以,尽可能使用最新的PHP版本,是最简单、最有效的性能优化方法之一。

2015年,PHP 7.0还没有正式发布(2015年12月发布),但PHP 5.6已经是比较成熟的版本。如果条件允许,建议升级到PHP 5.6或更高版本。

2. 使用OPcache

OPcache,是PHP的字节码缓存扩展。它会把PHP脚本编译后的字节码,缓存到内存中,下次访问时,直接从内存中读取字节码执行,不需要再次编译。

OPcache,能显著提升PHP的性能,通常能提升2-3倍。

开启OPcache: 在php.ini中配置:

[opcache]
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.revalidate_freq=60
opcache.fast_shutdown=1

注意

  • 开发环境,可以设置opcache.revalidate_freq=0,每次都检查文件是否修改,方便开发
  • 生产环境,可以设置opcache.revalidate_freq=60或更大,减少检查次数,提升性能
  • 部署代码后,可以调用opcache_reset()重置缓存,让新代码生效

3. 避免在循环中做重复操作

这是最常见的性能问题之一。

不好的写法

// 每次循环都调用count(),重复计算
for ($i = 0; $i < count($array); $i++) {
    // ...
}

// 每次循环都连接数据库
foreach ($items as $item) {
    $pdo = new PDO(...); // 每次都新建连接
    // ...
}

// 每次循环都查询数据库
foreach ($users as $user) {
    $profile = $db->query("SELECT * FROM profiles WHERE user_id = " . $user['id'])->fetch(); // N+1查询
    // ...
}

好的写法

// 先计算count,再循环
$count = count($array);
for ($i = 0; $i < $count; $i++) {
    // ...
}

// 循环外连接数据库,循环内复用
$pdo = new PDO(...);
foreach ($items as $item) {
    // 复用$pdo
    // ...
}

// 一次性查询所有数据,避免N+1查询
$userIds = array_column($users, 'id');
$profiles = $db->query("SELECT * FROM profiles WHERE user_id IN (" . implode(',', $userIds) . ")")->fetchAll();
$profileMap = [];
foreach ($profiles as $profile) {
    $profileMap[$profile['user_id']] = $profile;
}
foreach ($users as $user) {
    $profile = $profileMap[$user['id']] ?? null;
    // ...
}

4. 选择合适的字符串处理函数

PHP的字符串处理函数,性能差异很大。

  • strposstrstr快,因为strpos只需要找到位置,不需要返回子串
  • issetarraykeyexists快,因为isset是语言结构,不是函数
  • strreplacepregreplace快,简单替换用str_replace,复杂模式才用正则
  • 单引号字符串比双引号字符串快,因为单引号字符串不需要解析变量

示例

// 不好:用正则做简单替换
$result = preg_replace('/foo/', 'bar', $string);

// 好:用str_replace做简单替换
$result = str_replace('foo', 'bar', $string);

// 不好:用双引号字符串
$string = "Hello, $name!";

// 好:用单引号字符串拼接
$string = 'Hello, ' . $name . '!';

5. 引用传递,避免大数组复制

PHP默认是值传递,函数参数是数组时,会复制整个数组。如果数组很大,复制会消耗大量内存和时间。

使用引用传递(&),可以避免数组复制,提升性能。

示例

// 不好:值传递,大数组会被复制
function processArray($array) {
    // ...
}

// 好:引用传递,避免数组复制
function processArray(&$array) {
    // ...
}

注意:引用传递要谨慎使用,因为函数内对数组的修改,会影响原数组。如果不需要修改数组,只是读取,可以用引用传递提升性能;如果需要修改数组,要确保修改是预期的。

6. 提前退出,减少嵌套

深层嵌套的代码,不仅可读性差,性能也可能较差(因为需要维护更多的栈帧)。

使用"提前退出"(early return),可以减少嵌套,提升可读性和性能。

不好的写法

function process($user) {
    if ($user) {
        if ($user['is_active']) {
            if ($user['age'] >= 18) {
                // 处理逻辑
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    } else {
        return false;
    }
}

好的写法

function process($user) {
    if (!$user) return false;
    if (!$user['is_active']) return false;
    if ($user['age'] < 18) return false;
    
    // 处理逻辑
    return true;
}

7. 避免使用全局变量

全局变量,不仅破坏封装,增加耦合,也可能影响性能(因为全局变量需要在全局符号表中查找)。

尽量使用局部变量,或者通过参数传递,避免使用全局变量。

8. 关闭不必要的扩展

PHP加载的扩展越多,启动越慢,内存占用越多。

只加载需要的扩展,关闭不需要的扩展,可以提升PHP的启动速度,减少内存占用。

在php.ini中,注释掉不需要的扩展:

;extension=php_ldap.dll
;extension=php_soap.dll

数据库层面优化

数据库,通常是Web应用的性能瓶颈。数据库优化,是性能优化的重点。

1. 使用索引

索引,是数据库查询优化的最重要手段。合适的索引,能让查询速度提升几个数量级。

索引的类型

  • 主键索引:主键自动创建索引,唯一且非空
  • 唯一索引:值唯一,可以为空
  • 普通索引:最基本的索引
  • 联合索引:多个字段组成的索引,遵循"最左前缀原则"
  • 全文索引:用于全文搜索

索引的使用原则

  • 在WHERE、JOIN、ORDER BY、GROUP BY的字段上建索引
  • 区分度高的字段,适合建索引(如用户ID、邮箱);区分度低的字段,不适合建索引(如性别、状态)
  • 联合索引,把区分度高的字段放前面
  • 不要过度建索引,索引会增加写入的开销,占用存储空间
  • 定期检查索引使用情况,删除未使用的索引

示例

-- 给email字段建唯一索引
ALTER TABLE users ADD UNIQUE INDEX idx_email (email);

-- 给category_id和created_at建联合索引
ALTER TABLE posts ADD INDEX idx_category_created (category_id, created_at);

2. 避免SELECT *

SELECT *会查询所有字段,即使不需要的字段也会查询,浪费内存和网络带宽。

只查询需要的字段,能减少数据传输,提升性能。

不好的写法

SELECT * FROM users WHERE id = 1;

好的写法

SELECT id, name, email FROM users WHERE id = 1;

3. 避免N+1查询

N+1查询,是ORM和循环查询中常见的性能问题。

比如,先查询10篇文章(1次查询),然后循环每篇文章,查询作者信息(10次查询),总共11次查询,这就是N+1查询。

解决方法:使用JOIN一次性查询,或者使用IN批量查询。

示例

// 不好:N+1查询
$posts = $db->query("SELECT * FROM posts LIMIT 10")->fetchAll();
foreach ($posts as $post) {
    $author = $db->query("SELECT * FROM users WHERE id = " . $post['author_id'])->fetch();
    // ...
}

// 好:批量查询
$posts = $db->query("SELECT * FROM posts LIMIT 10")->fetchAll();
$authorIds = array_column($posts, 'author_id');
$authors = $db->query("SELECT * FROM users WHERE id IN (" . implode(',', $authorIds) . ")")->fetchAll();
$authorMap = [];
foreach ($authors as $author) {
    $authorMap[$author['id']] = $author;
}
foreach ($posts as $post) {
    $author = $authorMap[$post['author_id']] ?? null;
    // ...
}

4. 使用预处理语句

预处理语句(Prepared Statements),不仅能防止SQL注入,还能提升性能。

预处理语句,会把SQL语句发送给数据库编译,然后可以多次执行,只需要传递参数。对于多次执行的相同SQL,预处理语句能减少数据库编译的开销,提升性能。

示例

// 使用PDO预处理
$stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (:name, :email)");
foreach ($users as $user) {
    $stmt->execute([':name' => $user['name'], ':email' => $user['email']]);
}

5. 合理使用事务

事务,能保证数据的一致性,但也会锁定数据,影响并发性能。

  • 事务要尽量短,不要在事务中做耗时的操作(如发送邮件、调用API)
  • 批量操作,使用事务,能显著提升性能(因为每次INSERT都会提交事务,批量操作只提交一次)
  • 选择合适的事务隔离级别,隔离级别越高,并发性能越差

示例

// 批量插入,使用事务
$pdo->beginTransaction();
try {
    $stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (?, ?)");
    foreach ($users as $user) {
        $stmt->execute([$user['name'], $user['email']]);
    }
    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollBack();
    throw $e;
}

6. 分页优化

大数据量的分页,LIMIT offset, count会越来越慢,因为MySQL需要扫描offset条记录,然后丢弃。

优化方法:

  • 使用"游标分页"(基于上一页的最后一条记录的ID)
  • 限制最大页数,不允许翻到太后面
  • 使用延迟关联,先查ID,再关联查询详情

示例

-- 不好:offset越大越慢
SELECT * FROM posts ORDER BY id LIMIT 100000, 10;

-- 好:游标分页
SELECT * FROM posts WHERE id < 100000 ORDER BY id DESC LIMIT 10;

-- 好:延迟关联
SELECT p.* FROM posts p
INNER JOIN (SELECT id FROM posts ORDER BY id LIMIT 100000, 10) t ON p.id = t.id;

7. 数据库配置优化

MySQL的配置,对性能影响很大。

关键配置:

  • innodbbufferpool_size:InnoDB缓冲池大小,建议设为物理内存的50%-70%
  • innodblogfile_size:日志文件大小,建议256M-1G
  • innodbflushlogattrx_commit:事务提交时刷日志的策略,1是最安全,2是性能最好
  • querycachetypequerycachesize:查询缓存,MySQL 8.0已移除,之前版本可以开启,但要注意失效问题
  • max_connections:最大连接数,根据服务器配置和应用需求设置
  • tmptablesizemaxheaptable_size:临时表大小,避免临时表落盘

缓存层面优化

缓存,是提升Web应用性能的最有效手段之一。通过缓存,可以减少数据库查询,减少计算,提升响应速度。

1. 页面缓存

页面缓存,是把整个页面的HTML缓存起来,下次访问时,直接返回缓存的HTML,不需要执行PHP和查询数据库。

页面缓存,适合内容不经常变化的页面,如文章详情页、博客首页等。

实现方式

  • 使用Nginx的proxycache或fastcgicache
  • 使用PHP的输出缓冲,把页面内容存到文件或Redis
  • 使用CDN(内容分发网络)

2. 数据缓存

数据缓存,是把数据库查询的结果,缓存起来,下次查询时,直接从缓存读取,不需要查询数据库。

常用的缓存工具

  • Redis:高性能的键值存储,支持多种数据结构,支持持久化,是目前最流行的缓存工具
  • Memcached:简单的键值存储,纯内存,不支持持久化,性能也很高

缓存策略

  • Cache Aside:先查缓存,缓存没有再查数据库,然后写入缓存
  • Write Through:写数据时,同时写缓存和数据库
  • Write Behind:写数据时,只写缓存,异步写数据库(性能高,但有数据丢失风险)

缓存问题

  • 缓存穿透:查询不存在的数据,缓存和数据库都没有,每次都查数据库。解决:缓存空值,或使用布隆过滤器
  • 缓存雪崩:大量缓存同时失效,请求全部打到数据库。解决:缓存过期时间加随机值,或使用多级缓存
  • 缓存击穿:某个热点key失效,大量请求同时打到数据库。解决:加互斥锁,或热点key永不过期

示例

// Cache Aside模式
function getUser($id) {
    global $redis, $pdo;
    $key = "user:$id";
    $user = $redis->get($key);
    if ($user === false) {
        // 缓存没有,查数据库
        $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$id]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($user) {
            // 写入缓存,过期时间1小时
            $redis->setex($key, 3600, json_encode($user));
        } else {
            // 缓存空值,防止穿透,过期时间短一些
            $redis->setex($key, 60, json_encode(null));
        }
    } else {
        $user = json_decode($user, true);
    }
    return $user;
}

3. 片段缓存

片段缓存,是把页面的一部分(如侧边栏、热门文章列表)缓存起来,而不是整个页面。

片段缓存,适合页面中部分内容变化、部分内容不变的场景。

4. 浏览器缓存

通过设置HTTP响应头,让浏览器缓存静态资源(如CSS、JS、图片),减少重复下载。

常用的缓存头

  • Expires:设置资源的过期时间
  • Cache-Control:更灵活的缓存控制,如max-age=3600表示缓存1小时
  • ETag:资源的版本标识,资源变化时ETag变化
  • Last-Modified:资源的最后修改时间

Nginx配置示例

location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}

服务器层面优化

1. 使用Nginx + PHP-FPM

Nginx + PHP-FPM,是目前PHP生产环境的标准配置,性能比Apache + mod_php好很多。

Nginx是事件驱动的Web服务器,高并发性能好;PHP-FPM是PHP的FastCGI进程管理器,能高效管理PHP进程。

2. PHP-FPM配置优化

PHP-FPM的配置,对性能影响很大。

关键配置(php-fpm.conf或www.conf):

  • pm:进程管理方式,dynamic(动态,默认)、static(静态,固定进程数)、ondemand(按需,有请求才创建进程)
  • pm.max_children:最大进程数,根据服务器内存设置,每个PHP进程约占20-50M内存
  • pm.start_servers:启动时的进程数(dynamic模式)
  • pm.minspareservers:最小空闲进程数(dynamic模式)
  • pm.maxspareservers:最大空闲进程数(dynamic模式)
  • pm.max_requests:每个进程处理多少请求后重启,防止内存泄漏

示例配置

pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500

3. Nginx配置优化

Nginx的配置,对性能也有影响。

关键配置:

  • worker_processes:工作进程数,建议设为CPU核数
  • worker_connections:每个工作进程的最大连接数
  • keepalive_timeout:长连接超时时间
  • gzip:开启Gzip压缩,减少传输体积
  • clientmaxbody_size:最大请求体大小

示例配置

worker_processes auto;
events {
    worker_connections 1024;
}
http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    
    server {
        listen 80;
        server_name example.com;
        root /var/www/html;
        index index.php index.html;
        
        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }
        
        location ~ \.php$ {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
    }
}

4. 开启Gzip压缩

Gzip压缩,能显著减少HTTP响应的体积,提升传输速度。

在Nginx中开启Gzip:

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;

5. 使用CDN

CDN(内容分发网络),能把静态资源缓存到离用户最近的节点,提升访问速度,减轻源站压力。

对于图片、CSS、JS等静态资源,使用CDN,能显著提升性能。

其他优化技巧

1. 压缩图片

图片,通常是网页中体积最大的资源。压缩图片,能显著减少网页加载时间。

  • 使用合适的图片格式:JPG适合照片,PNG适合透明图片,WebP体积更小(2015年WebP还不太普及,现在已经很流行)
  • 压缩图片质量,在视觉可接受的范围内,尽量压缩
  • 使用响应式图片,根据设备屏幕大小,加载不同尺寸的图片
  • 使用CSS雪碧图(Sprite),把多张小图合并成一张,减少HTTP请求

2. 合并和压缩CSS/JS

合并多个CSS/JS文件,减少HTTP请求;压缩CSS/JS,减少文件体积。

  • 使用构建工具(如Gulp、Grunt、Webpack)自动合并和压缩
  • CSS放在页面头部,JS放在页面底部,避免阻塞页面渲染
  • 使用异步加载JS(asyncdefer

3. 减少HTTP请求

HTTP请求,有建立连接的开销。减少HTTP请求,能提升页面加载速度。

  • 合并CSS/JS文件
  • 使用CSS雪碧图
  • 使用字体图标(如Font Awesome)代替图片图标
  • 小图片使用Base64内嵌
  • 使用HTTP/2(多路复用,减少请求开销)

4. 使用CDN和静态资源分离

把静态资源(图片、CSS、JS)放到单独的域名或CDN,和动态页面分离,能提升并发性能,也方便缓存。

性能优化的误区

1. 过度优化

不要为了一点点性能提升,而把代码搞得很复杂,降低可读性和可维护性。性能优化,要权衡成本和收益。

2. 只优化代码,不优化数据库和架构

很多性能问题,不是代码的问题,而是数据库、架构的问题。只优化代码,效果有限。要全方位优化,包括代码、数据库、缓存、服务器、架构。

3. 忽略网络和前端

Web应用的性能,不仅取决于后端PHP,还取决于网络传输和前端渲染。很多时候,页面加载慢,是因为图片大、CSS/JS多、网络慢,而不是PHP慢。

4. 相信"银弹"

没有万能的优化方法。不同的应用,性能瓶颈不同,优化方法也不同。要根据实际情况,测量、分析、优化。

总结

PHP性能优化要点:

  1. 优化原则:不要过早优化、先测量再优化、优化瓶颈、权衡成本收益、优化后验证
  2. 代码层面:使用最新PHP版本、开启OPcache、避免循环中重复操作、选择合适的字符串函数、引用传递避免大数组复制、提前退出减少嵌套、避免全局变量、关闭不必要扩展
  3. 数据库层面:使用索引、避免SELECT *、避免N+1查询、使用预处理语句、合理使用事务、分页优化、数据库配置优化
  4. 缓存层面:页面缓存、数据缓存(Redis/Memcached,注意缓存穿透/雪崩/击穿)、片段缓存、浏览器缓存
  5. 服务器层面:Nginx+PHP-FPM、PHP-FPM配置优化、Nginx配置优化、开启Gzip、使用CDN
  6. 其他优化:压缩图片、合并压缩CSS/JS、减少HTTP请求、CDN和静态资源分离
  7. 误区:过度优化、只优化代码不优化数据库和架构、忽略网络和前端、相信银弹

性能优化,是一个持续的过程,不是一次性的工作。随着业务的增长,用户量的增加,新的性能问题会不断出现。我们需要持续关注性能,持续测量,持续优化。

"性能,不是一蹴而就的,而是持续改进的结果。"希望这篇文章,能帮你掌握PHP性能优化的方法和技巧,让你的代码飞起来。

最后,记住:先测量,再优化;优化瓶颈,而不是全部;权衡成本和收益。 这是性能优化的金科玉律。