性能,是Web开发中永恒的话题。一个性能好的网站,加载速度快,用户体验好,搜索引擎排名也高;一个性能差的网站,加载缓慢,用户流失,转化率低。

PHP,作为一门解释型语言,性能一直是被诟病的地方。但实际上,只要掌握了正确的优化方法,PHP也能跑得很快。很多时候,PHP慢,不是因为语言本身,而是因为代码写得不好,数据库查询不合理,没有使用缓存。

在之前的文章中,我们学习了PHP的各种功能开发。今天,我们来聊聊PHP性能优化,分享10个从慢到快的关键技巧,帮你让PHP网站跑得更快。

1. 使用OPcache

OPcache,是PHP官方的字节码缓存扩展。它会把PHP脚本编译后的字节码缓存起来,下次请求时,直接从缓存中读取字节码执行,不需要重新编译。

原理: PHP是解释型语言,每次请求,PHP都需要:读取文件→词法分析→语法分析→编译为字节码→执行字节码。这个编译过程,会消耗一定的时间。OPcache把编译后的字节码缓存到内存中,下次请求直接执行,跳过了编译过程。

配置(php.ini):

[opcache]
zend_extension=opcache.so  ; Windows是php_opcache.dll
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=128       ; 共享内存大小,单位MB
opcache.interned_strings_buffer=8     ;  interned字符串内存,单位MB
opcache.max_accelerated_files=10000   ; 最大缓存文件数
opcache.revalidate_freq=60             ; 检查文件更新的频率,单位秒
opcache.fast_shutdown=1                ; 快速关闭
opcache.enable_file_override=0
opcache.optimization_level=0x7FFFBFFF ; 优化级别

效果: 开启OPcache后,PHP性能通常能提升2-5倍。这是最简单、最有效的PHP性能优化手段,几乎零成本,强烈建议所有生产环境都开启。

注意事项

  • 开发环境可以把opcache.revalidate_freq设为0,每次请求都检查文件更新,方便开发。
  • 生产环境可以设为60或更大,减少文件检查的开销。
  • 代码更新后,可以调用opcache_reset()重置缓存,或重启PHP-FPM。

2. 数据库查询优化

数据库查询,通常是Web应用中最慢的部分。一个不合理的SQL查询,可能比整个PHP脚本的执行时间还长。

2.1 使用索引

索引,是数据库查询优化的基础。没有索引的查询,会全表扫描,数据量大时非常慢。

-- 为常用查询字段添加索引
ALTER TABLE blog_posts ADD INDEX idx_category_id (category_id);
ALTER TABLE blog_posts ADD INDEX idx_status (status);
ALTER TABLE blog_posts ADD INDEX idx_created_at (created_at);

-- 联合索引(注意最左前缀原则)
ALTER TABLE blog_posts ADD INDEX idx_status_category (status, category_id);

索引使用原则

  • 为WHERE、JOIN、ORDER BY、GROUP BY的字段添加索引
  • 联合索引遵循最左前缀原则
  • 不要过度索引,索引会降低INSERT/UPDATE/DELETE的性能
  • EXPLAIN分析查询计划,查看是否使用了索引

2.2 避免SELECT *

只查询需要的字段,不要用SELECT *

-- 不好
SELECT * FROM blog_posts WHERE id = 1;

-- 好
SELECT id, title, slug, content, created_at FROM blog_posts WHERE id = 1;

SELECT *会查询所有字段,增加网络传输和内存消耗,也无法利用覆盖索引。

2.3 批量查询,避免N+1问题

在循环中查询数据库,是常见的性能问题(N+1查询)。

// 不好:N+1查询
$posts = $db->query("SELECT * FROM blog_posts LIMIT 10")->fetchAll();
foreach ($posts as $post) {
    // 每次循环都查询一次数据库
    $category = $db->query("SELECT * FROM categories WHERE id = {$post['category_id']}")->fetch();
    $post['category'] = $category;
}

// 好:批量查询
$posts = $db->query("SELECT * FROM blog_posts LIMIT 10")->fetchAll();
$categoryIds = array_column($posts, 'category_id');
$categories = $db->query("SELECT * FROM categories WHERE id IN (" . implode(',', $categoryIds) . ")")->fetchAll(PDO::FETCH_KEY_PAIR);
foreach ($posts as &$post) {
    $post['category'] = $categories[$post['category_id']] ?? null;
}

2.4 使用LIMIT分页

查询大量数据时,用LIMIT限制返回条数。

-- 分页查询
SELECT * FROM blog_posts WHERE status = 'published' ORDER BY created_at DESC LIMIT 20 OFFSET 0;

深分页优化:当OFFSET很大时,性能会下降。可以用延迟关联或游标分页优化。

-- 延迟关联优化深分页
SELECT p.* FROM blog_posts p
INNER JOIN (SELECT id FROM blog_posts WHERE status = 'published' ORDER BY created_at DESC LIMIT 20 OFFSET 1000) t ON p.id = t.id;

2.5 用EXPLAIN分析查询

EXPLAIN SELECT * FROM blog_posts WHERE status = 'published' ORDER BY created_at DESC LIMIT 20;

关注type(访问类型,最好是ref/range,避免ALL全表扫描)、key(使用的索引)、rows(扫描的行数)、Extra(额外信息,避免Using filesort、Using temporary)。

3. 使用缓存

缓存,是性能优化的利器。把频繁访问、不经常变化的数据缓存起来,下次直接从缓存读取,避免重复计算和数据库查询。

3.1 Redis缓存

Redis,是最常用的缓存系统,性能极高,支持多种数据结构。

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// 缓存文章详情
$cacheKey = "post:{$id}";
$post = $redis->get($cacheKey);
if ($post === false) {
    // 缓存未命中,从数据库查询
    $post = $db->prepare("SELECT * FROM blog_posts WHERE id = ?")->execute([$id])->fetch();
    // 写入缓存,过期时间1小时
    $redis->setex($cacheKey, 3600, json_encode($post));
} else {
    $post = json_decode($post, true);
}

3.2 缓存策略

  • 缓存过期:设置合理的过期时间(TTL),避免数据永远不更新
  • 缓存更新:数据更新时,主动删除或更新缓存
  • 缓存穿透:查询不存在的数据,缓存永远不命中。解决:缓存空值、布隆过滤器
  • 缓存雪崩:大量缓存同时过期,请求全部打到数据库。解决:过期时间加随机值、多级缓存
  • 缓存击穿:热点key过期,大量请求同时查询数据库。解决:互斥锁、永不过期

3.3 页面缓存

对于不经常变化的页面,可以把整个页面缓存起来。

$cacheKey = "page:home:" . md5($_SERVER['REQUEST_URI']);
if ($redis->exists($cacheKey)) {
    echo $redis->get($cacheKey);
    exit;
}

// 生成页面内容
ob_start();
// ... 渲染页面 ...
$content = ob_get_clean();

// 缓存页面,5分钟过期
$redis->setex($cacheKey, 300, $content);
echo $content;

4. 代码层面优化

4.1 减少文件包含

PHP的文件包含(include/require)有一定开销,避免不必要的文件包含。

  • 使用自动加载(splautoloadregister),只在需要时加载类文件
  • 不要在循环中include文件
  • 合并小文件,减少文件数量

4.2 避免在循环中做耗时操作

// 不好:每次循环都计算count
for ($i = 0; $i < count($array); $i++) {
    // ...
}

// 好:提前计算count
$count = count($array);
for ($i = 0; $i < $count; $i++) {
    // ...
}

4.3 使用单引号代替双引号

PHP中,单引号字符串不会解析变量,比双引号快。

// 单引号更快
$str = 'Hello World';

// 双引号会解析变量,略慢
$str = "Hello $name";

不过,这个差异在现代PHP中已经很小了,不必过度纠结,保持代码可读性更重要。

4.4 用isset()代替arraykeyexists()

// isset()更快,但如果值为null会返回false
if (isset($array['key'])) {
    // ...
}

// array_key_exists()即使值为null也返回true,但更慢
if (array_key_exists('key', $array)) {
    // ...
}

4.5 避免不必要的正则

正则表达式的执行开销较大,能用字符串函数解决的,就不要用正则。

// 不好:用正则判断前缀
if (preg_match('/^http:/', $url)) {
    // ...
}

// 好:用字符串函数
if (strpos($url, 'http:') === 0) {
    // ...
}

4.6 释放不需要的变量

大变量用完后,及时unset释放内存。

$bigData = fetchHugeData();
// ... 处理 $bigData ...
unset($bigData); // 释放内存

5. 使用PHP 7+

PHP 7,是PHP性能的一次巨大飞跃。PHP 7的性能,比PHP 5.6提升了2-3倍,内存消耗也大幅降低。

PHP 7的新特性:

  • 全新的Zend引擎(Zend Engine 3),性能大幅提升
  • 标量类型声明(int、float、string、bool)
  • 返回类型声明
  • 匿名类
  • 太空船操作符(<=>)
  • null合并操作符(??)
  • 常量数组
  • 错误处理改进(Error类)

如果还在使用PHP 5,强烈建议升级到PHP 7或更高版本。PHP 5.6已经在2018年底停止维护,不再有安全更新。

2015年,PHP 7刚刚发布(2015年12月),很多网站还在使用PHP 5.6。但从性能角度,升级到PHP 7是值得的。

6. Web服务器优化

6.1 Nginx优化

# 开启gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss;

# 静态文件缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|eot)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}

# 开启sendfile
sendfile on;
tcp_nopush on;
tcp_nodelay on;

# 连接超时
keepalive_timeout 65;

6.2 PHP-FPM优化

[www]
user = www
group = www
listen = 127.0.0.1:9000
listen.backlog = -1

pm = dynamic
pm.max_children = 50        ; 最大子进程数
pm.start_servers = 5         ; 启动时的子进程数
pm.min_spare_servers = 5     ; 最小空闲子进程数
pm.max_spare_servers = 35    ; 最大空闲子进程数
pm.max_requests = 500         ; 每个子进程处理多少请求后重启(防止内存泄漏)

request_terminate_timeout = 30  ; 请求超时时间

pm.maxchildren的设置,要根据服务器内存来定。一般每个PHP-FPM进程占用20-50MB内存,maxchildren = 可用内存 / 单进程内存

7. 静态资源优化

7.1 合并和压缩CSS/JS

  • 合并多个CSS/JS文件,减少HTTP请求数
  • 压缩CSS/JS,去除空格、注释,减小文件体积
  • 使用构建工具(如Gulp、Webpack)自动处理

7.2 图片优化

  • 使用合适的图片格式:照片用JPEG,图标用PNG,简单图形用SVG
  • 压缩图片,减小文件体积(如TinyPNG)
  • 使用响应式图片,根据设备加载不同尺寸的图片
  • 使用WebP等现代图片格式,体积更小
  • 图片懒加载,首屏外的图片延迟加载

7.3 CDN加速

使用CDN(内容分发网络),把静态资源缓存到离用户最近的节点,加快访问速度。

国内常用的CDN:阿里云CDN、腾讯云CDN、七牛云、又拍云等。

8. 异步处理

对于耗时的操作(如发送邮件、生成报表、推送通知),不要在请求中同步执行,应该异步处理。

8.1 消息队列

使用Redis队列、RabbitMQ、Kafka等消息队列,把任务放到队列中,由后台Worker异步处理。

// 生产者:把任务放入队列
$redis->lPush('email_queue', json_encode([
    'to' => 'user@example.com',
    'subject' => '欢迎注册',
    'body' => '欢迎加入我们!',
]));

// 消费者(后台脚本):从队列取出任务执行
while (true) {
    $task = $redis->rPop('email_queue');
    if ($task) {
        $data = json_decode($task, true);
        sendEmail($data['to'], $data['subject'], $data['body']);
    }
    usleep(100000); // 100ms
}

8.2 定时任务

对于不需要实时处理的任务,用定时任务(Crontab)批量处理。

# 每天凌晨2点执行统计任务
0 2 * * * /usr/bin/php /path/to/statistics.php

9. 性能监控和分析

优化性能,首先要知道瓶颈在哪里。使用性能分析工具,找到慢的地方,针对性优化。

9.1 Xdebug Profiler

Xdebug的Profiler功能,可以分析PHP脚本的执行时间,找到耗时的函数。

[xdebug]
xdebug.profiler_enable = 1
xdebug.profiler_output_dir = /tmp/xdebug
xdebug.profiler_output_name = cachegrind.out.%t.%p

用KCacheGrind、WinCacheGrind、Webgrind等工具查看分析结果。

9.2 慢查询日志

开启MySQL慢查询日志,记录执行时间长的SQL。

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1  # 超过1秒的查询记录
log_queries_not_using_indexes = 1  # 记录没使用索引的查询

mysqldumpslow工具分析慢查询日志。

9.3 应用性能监控(APM)

使用APM工具(如New Relic、Datadog、听云、OneAPM),实时监控应用性能,找到瓶颈。

10. 架构层面优化

当单台服务器无法满足性能需求时,需要从架构层面优化。

10.1 负载均衡

使用Nginx、HAProxy、LVS等负载均衡器,把请求分发到多台应用服务器,提升并发处理能力。

10.2 读写分离

数据库读写分离,主库写,从库读,提升数据库并发能力。

10.3 分库分表

当单表数据量太大时,进行分库分表,提升查询性能。

10.4 微服务

把单体应用拆分为多个微服务,每个服务独立部署、独立扩展,提升整体性能和可维护性。

不过,架构层面的优化,复杂度高,成本也高,只有在单台服务器确实无法满足需求时才考虑。不要过早优化,先把代码、数据库、缓存这些基础优化做好。

性能优化的原则

  1. 先测量,再优化:不要凭感觉优化,先用工具找到瓶颈,再针对性优化。
  2. 避免过早优化:先把功能做对,再优化性能。过早优化会增加复杂度,降低开发效率。
  3. 80/20原则:80%的性能问题,来自20%的代码。找到那20%的瓶颈,重点优化。
  4. 权衡取舍:性能优化,往往需要在时间、空间、复杂度、成本之间权衡。没有最优,只有最适合。
  5. 持续优化:性能优化不是一次性的,而是持续的过程。随着业务增长,数据量增加,会出现新的性能问题,需要持续优化。
  6. 不要牺牲可读性:为了微小的性能提升,牺牲代码可读性和可维护性,是不值得的。

总结

PHP性能优化,是一个系统工程,涉及多个层面。

10个关键技巧:

  1. 使用OPcache:字节码缓存,最简单最有效,性能提升2-5倍
  2. 数据库查询优化:使用索引、避免SELECT *、批量查询避免N+1、LIMIT分页、EXPLAIN分析
  3. 使用缓存:Redis缓存数据和页面,注意缓存穿透/雪崩/击穿
  4. 代码层面优化:减少文件包含、避免循环中耗时操作、单引号、isset、避免不必要正则、释放变量
  5. 使用PHP 7+:性能比PHP 5提升2-3倍
  6. Web服务器优化:Nginx gzip/静态缓存/sendfile,PHP-FPM进程管理
  7. 静态资源优化:合并压缩CSS/JS、图片优化、CDN加速
  8. 异步处理:消息队列、定时任务,耗时操作异步执行
  9. 性能监控和分析:Xdebug Profiler、慢查询日志、APM工具,找到瓶颈
  10. 架构层面优化:负载均衡、读写分离、分库分表、微服务(按需使用)

性能优化原则:先测量再优化、避免过早优化、80/20原则、权衡取舍、持续优化、不牺牲可读性。

性能优化,没有终点。但只要掌握了正确的方法,一步一步来,就能让PHP网站跑得越来越快。

" premature optimization is the root of all evil.(过早优化是万恶之源)"——Donald Knuth。但在需要优化的时候,也要有能力优化。希望这篇文章,能帮你在需要的时候,做好PHP性能优化。