本文约 5,319 字,预计阅读 14 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:同一个文件被加载了三次
用 PageSpeed 或浏览器 Network 面板一看,jquery.min.js 出现了三条请求,font-awesome.css 有两条,某个滑块插件的 JS 四个插件各加载一遍。结果是控制台报 $(...).slider is not a function、动画执行两遍、页面体积翻倍、LCP 一路飘红。
更让人困惑的是,你在 functions.php 里写了 wp_dequeue_script( 'jquery' ),刷新一看它还在。这通常不是函数写错了,而是执行时机不对:资源注册与出队发生在 wp_enqueue_scripts 钩子的不同阶段,你在 priority 10 摘掉的句柄,另一个插件在 priority 20 又注册了一遍。
要彻底解决,得先理解 WordPress 的资源管理系统(WP_Scripts / WP_Styles)。
原理分析:句柄是唯一键,依赖图决定顺序
WordPress 的每份 JS/CSS 都通过「句柄(handle)」标识,全站唯一。wp_register_script() 把脚本信息(src、版本、依赖、是否页脚)写进 WP_Scripts::$registered;wp_enqueue_script() 把句柄放进 WP_Scripts::$queue;最后 wp_print_scripts 按依赖拓扑排序输出 <script>。
重复加载有三种形态:
| 形态 | 成因 | 表现 | 解法 |
|---|---|---|---|
| 同一 handle 注册两次不同 src | 两个插件争抢同一个通用句柄(如 jquery、font-awesome) |
后注册覆盖先注册,某个插件功能失效 | 改名注册或统一由一方提供 |
| 不同 handle 指向同一文件 | 各插件各自打包了自己的副本 | 同一文件多次 HTTP 请求 | 按 src 去重 |
| 内联脚本重复 | wp_add_inline_script 被多次调用 |
同一段初始化逻辑执行多遍 | 加一次性开关 |
还有一个容易被忽略的事实:wp_register_script 重复注册同一句柄时,后注册者直接覆盖前者。所以「谁最后执行谁说了算」,这就是为什么资源类问题的根因往往是执行顺序,而不是代码写错。
wp_dequeue_script() 和 wp_deregister_script() 的区别:前者只把句柄从队列里摘掉(注册信息还在,仍可被别的脚本依赖并因此被重新拉起);后者把注册信息整个删掉(依赖它的脚本会拿不到依赖,可能导致别的脚本根本不输出)。绝大多数场景该用 dequeue,deregister 只在你要彻底替换某个句柄(例如用 CDN 版 jQuery 替换自带版)时才用。
保姆级实操
第一步:盘点到底哪些文件被重复加载
wp eval '
$w = wp_scripts();
$map = [];
foreach ( $w->queue as $h ) {
$d = $w->registered[$h] ?? null;
if ( $d && $d->src ) { $map[ strtok( $d->src, "?" ) ][] = $h; }
}
foreach ( $map as $src => $handles ) {
if ( count( $handles ) > 1 ) { echo $src, " => ", implode( ", ", $handles ), PHP_EOL; }
}'
把 wp_scripts() 换成 wp_styles() 就能查样式。输出形如:
/wp-includes/js/jquery/jquery.min.js => jquery, jquery-core, plugin-a-jquery
/wp-content/plugins/slider-x/slider.js => slider-x, slider-x-pro
第二步:按 src 去重(通用兜底)
不改任何插件文件,在前台输出前做一次统一去重,保留第一个出现在队列里的句柄:
<?php
// mu-plugins/dedupe-assets.php
add_action( 'wp_enqueue_scripts', function () {
$scripts = wp_scripts();
$seen = [];
foreach ( array_values( $scripts->queue ) as $handle ) {
$dep = $scripts->registered[ $handle ] ?? null;
if ( ! $dep || ! $dep->src ) {
continue; // 没有 src 的多半是内联占位,跳过
}
$src = strtok( $dep->src, '?' ); // 去掉 ?ver= 再比较
if ( isset( $seen[ $src ] ) ) {
wp_dequeue_script( $handle ); // 同一文件已有人排队,摘掉后来者
continue;
}
$seen[ $src ] = $handle;
}
}, PHP_INT_MAX );
PHP_INT_MAX 是关键:必须在所有插件都注册完之后再执行,否则会漏掉后注册的那批。
第三步:精准摘掉某一个插件的资源
去重兜底之外,针对明确知道要删的目标,写法是:
add_action( 'wp_enqueue_scripts', function () {
// 只在产品详情页摘掉滑块插件的样式,避免全站误伤
if ( is_singular( 'product' ) ) {
return;
}
wp_dequeue_style( 'slider-x-frontend' );
wp_dequeue_script( 'slider-x-frontend' );
}, 99 );
注意:只 dequeue 而不 deregister,如果另一个脚本声明了 slider-x-frontend 作为依赖,它仍然会被拉起来。要确认依赖链,用:
wp eval '$w = wp_scripts(); print_r( $w->registered["slider-x-frontend"]->deps );'
第四步:处理 jQuery 这类公共句柄
千万不要在后台(wp-admin)deregister jQuery——后台大量功能依赖 WordPress 自带版本的确切行为,换版本会让媒体库、编辑器直接崩掉。前台替换也要谨慎:
add_action( 'wp_enqueue_scripts', function () {
if ( is_admin() ) {
return;
}
// 只做"去重",不做"换版本":确保只有一个 jquery 句柄在队列里
if ( wp_script_is( 'plugin-a-jquery', 'enqueued' ) ) {
wp_dequeue_script( 'plugin-a-jquery' );
}
}, 100 );
如果主题和各插件都规规矩矩用 array( 'jquery' ) 声明依赖,WordPress 本来就只会输出一次。重复出现的根源通常是某个插件自己 wp_enqueue_script( 'plugin-a-jquery', 'https://cdn.../jquery.min.js' ) 绕开了系统,这种情况下只能按句柄手动摘除。
第五步:不要丢掉内联脚本与 data
wp_add_inline_script() 挂载的内联代码存储在 WP_Scripts::$registered[$handle]->extra 里,一旦你 wp_deregister_script() 把句柄删掉,这些内联代码会一起消失,导致「脚本换成了新的,但初始化没了」。安全做法是保留句柄只换 src:
add_action( 'wp_enqueue_scripts', function () {
$s = wp_scripts();
if ( isset( $s->registered['jquery-core'] ) ) {
// 换 src、保留 extra,避免内联代码丢失
$s->registered['jquery-core']->src = 'https://cdn.example.com/jquery.min.js';
$s->registered['jquery-core']->ver = '3.7.1';
}
}, 100 );
第六步:用 script_loader_tag 做最后兜底
个别资源无法在队列阶段处理时,可以在输出阶段过滤标签:
add_filter( 'script_loader_tag', function ( $tag, $handle, $src ) {
$block = [ 'plugin-a-analytics', 'duplicate-tracker' ];
if ( in_array( $handle, $block, true ) ) {
return ''; // 直接不输出
}
return $tag;
}, 10, 3 );
常见踩坑
-
priority 太小:在
wp_enqueue_scripts默认优先级(10)里 dequeue,别的插件在 20 又 enqueue 回来,看起来「dequeue 不生效」。 -
用 deregister 代替 dequeue:把
jquery整个注销后,所有声明array('jquery')依赖的脚本因为依赖不满足而不输出,前台功能大面积失效。 -
案例: 某站为了「优化」把
jquery-migrate全局 deregister,结果一个 2016 年的老滑块插件用了$.browser(jQuery 1.9 之后被移除、由 migrate 补齐),前台轮播彻底不动。教训:老站点上jquery-migrate是兼容垫,删它等于给老代码判死刑。 -
在
init或plugins_loaded上操作:此时资源还没注册,wp_dequeue_script无效。资源相关钩子只有wp_enqueue_scripts(前台)、admin_enqueue_scripts(后台)、login_enqueue_scripts(登录页)。 -
忘记后台是另一套:前台摘掉的资源后台还在,反之亦然。要在后台同理处理需挂
admin_enqueue_scripts。 -
合并插件掩盖问题:用 Autoptimize、WP Rocket 的文件合并功能后,重复不再表现为多次请求,但重复的内联初始化逻辑仍会执行两次,动画依然双跑。
-
CDN 回源失败:把
jquery指向外部 CDN 后,国内用户访问该 CDN 超时,页面卡住十几秒才继续。国内站点优先用本地或国内 CDN。WordPress 国内 CDN 选型
总结
判断标准:能用依赖声明解决的,绝不手动 dequeue;要去重就按 src 在 PHP_INT_MAX 优先级统一兜底;要替换就保留句柄只换 src,别丢 extra;后台的 jQuery 一个字都别动。
行动清单:先用 wp eval 导出重复 src 清单;写 mu-plugin 做全局 src 去重;针对个别插件用 wp_dequeue_style/script 在 priority 99 精准摘除;检查被摘句柄是否被别人依赖;改完用 DevTools 复核请求数与控制台报错;把去重规则写进运维文档,避免每次装新插件都要重新踩一遍。WordPress 前端性能优化清单
