搜索全站
插件 14 分钟阅读

WordPress 重复加载 JS/CSS:句柄冲突与 deregister 用法

同一个 jquery.js 被加载三遍,控制台一堆 is not a function?本文讲清 WordPress 句柄与依赖图机制,给出重复资源盘点脚本、dequeue/deregister 正确时机与内联脚本丢失的补救办法。

U
UFO_PF 2020-05-17 · 阅读 14 分钟
Wo 插件
阅读提示
本文约 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 );

常见踩坑

  1. priority 太小:在 wp_enqueue_scripts 默认优先级(10)里 dequeue,别的插件在 20 又 enqueue 回来,看起来「dequeue 不生效」。

  2. 用 deregister 代替 dequeue:把 jquery 整个注销后,所有声明 array('jquery') 依赖的脚本因为依赖不满足而不输出,前台功能大面积失效。

  3. 案例: 某站为了「优化」把 jquery-migrate 全局 deregister,结果一个 2016 年的老滑块插件用了 $.browser(jQuery 1.9 之后被移除、由 migrate 补齐),前台轮播彻底不动。教训:老站点上 jquery-migrate 是兼容垫,删它等于给老代码判死刑。

  4. 在 init 或 plugins_loaded 上操作:此时资源还没注册,wp_dequeue_script 无效。资源相关钩子只有 wp_enqueue_scripts(前台)、admin_enqueue_scripts(后台)、login_enqueue_scripts(登录页)。

  5. 忘记后台是另一套:前台摘掉的资源后台还在,反之亦然。要在后台同理处理需挂 admin_enqueue_scripts。

  6. 合并插件掩盖问题:用 Autoptimize、WP Rocket 的文件合并功能后,重复不再表现为多次请求,但重复的内联初始化逻辑仍会执行两次,动画依然双跑。

  7. 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 前端性能优化清单

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。