普通视图

发现新文章,点击刷新页面。
昨天 — 2026年3月28日首页

纯干货,前端字体极致优化!谷歌、阿里、字节、腾讯都在用的终极解决方案,Vue3 + Vite 直接抄,页面提速不妥协!

作者 李剑一
2026年3月28日 09:56

最近在做一个公网的小项目,本身是一个在线的海报编辑器,因为之前做的比较糙,最近有时间了,领导让优化一下。

问题主要集中在页面的加载速度上。

image.png

设计稿要求多字体质感、标题正文差异化排版,结果引入的字体包动辄几MB,中文字体甚至直奔10MB+。

页面首屏加载非常慢,但是删字体包设计那边过不去,不删吧用户等待时间过长,体验直线下滑,简直两难。

核心问题

不过别慌,稳住了!

先明确我们到底在解决什么问题,避免盲目优化。

其实主要几个问题:

  • 字体体积冗余:完整字体包包含上万字符,项目实际用到的不过几百个,甚至有可能就几个字,全量加载纯纯浪费。
  • 阻塞页面:字体包过大,加载过程中可能触发FOIT(文字隐形)、FOUT(文字闪烁),页面出现留白卡顿。
  • 多字体加载混乱:一个页面同时存在多种字体包同时引入的时候,基本上就是谁在前面加载谁,无规划加载拖慢整体渲染。
  • 格式不兼容:沿用TTF/OTF老式字体格式,体积大、压缩率低,完全适配不了现代前端性能要求。

但是格式问题需要注意,新式的字体包,比如说WOFF2,是不支持IE这种较老版本的浏览器的。

如果你有兼容需求,记得不要上新包。

解决方案

建议字体包转WOFF2

前提是你只要没有兼容性需求,几乎WOFF2必转的。

相比于传统TTF、OTF、WOFF格式,WOFF2是现代浏览器专属的字体压缩格式,体积能直接缩减50%-70%

一个TTF大约20MB的包,在WOFF2上大概也就8MB左右,这个优化是显而易见的。

而且Chrome、Edge、Safari、Firefox全版本兼容,完全不用担心兼容性问题。

可以用 Font Squirrel在线工具进行一键转换,或者直接让UI那边导出来WOFF2的包。

按场景拆分字体包

多字体包的情况下千万别全量引入,一定要按照使用频率、使用场景拆分:

高频使用的优先处理,低频使用的单独拆分,延后加载。

可以在引入的部分,按照字重、字体样式拆分@font-face

这样的好处在于浏览器只会加载当前页面用到的字体规则,不会全量请求所有字体包。

/* 按字重拆分,按需加载 */
@font-face {
  font-family: 'SourceHanSans';
  src: url('./fonts/regular.woff2') format('woff2');
  font-weight: 400;
}
@font-face {
  font-family: 'SourceHanSans';
  src: url('./fonts/bold.woff2') format('woff2');
  font-weight: 700;
}

延迟加载

另外文字留白、闪烁问题,核心就是靠font-display属性。

使用font-display: swap浏览器会先使用系统默认字体展示页面文字,等到自定义字体加载完成后,再无缝替换。

全程不会出现文字隐形、页面卡顿的情况,让用户感知上有一种等一小会儿的感觉。

还有就是可视区域以外的字体,完全没必要和首屏一起加载,等到页面加载完成、或者用户触发对应模块时,再加载字体即可,减少首屏的请求数量。

// 页面加载完成后,懒加载非首屏字体
window.addEventListener('load', () => {
  const link = document.createElement('link');
  link.rel = 'stylesheet';
  link.href = '/css/other-font.css';
  document.head.appendChild(link);
})

字体子集化&按需加载(终极方案)

前面主要是给字体"减负",字体子集化和按需加载就是直接给字体"瘦身"。

据我所知,这也是目前谷歌、阿里、腾讯等大厂通用的天花板方案,能够彻底解决字体冗余问题。

核心原理

完整字体包中包含海量未使用字符,这样我们其实可以通过工具提取项目实际用到的字符

将大字体拆分成多个极小的字体分片;再通过CSS的unicode-range告诉浏览器,哪些字符对应哪个字体分片。

浏览器只会加载当前页面用到的分片,没用到的完全不请求。

相当于是对通过拆字的方式实现了对字体包的懒加载。

简单画一个流程图:

image.png

这套方案下来,原本几MB的字体,能直接压缩到几十KB,首屏字体加载速度提升数十倍,还完全不影响多字体使用!

具体实现

这里我推荐几个我用过的:glyphhangerfontminvite-plugin-fontmin

glyphhanger

基于Node.js,无需手动提取字符,直接爬取页面文字,自动生成子集字体+unicode-range CSS,适合快速优化现有页面,新手也能一键上手。

# 全局安装
npm install -g glyphhanger
# 一键生成子集字体与CSS
glyphhanger http://localhost:3000 --formats=woff2 --subset=./src/fonts/xxx.ttf

fontmin,纯JS定制化工具

纯JavaScript实现,无额外环境依赖,支持自定义提取字符、批量处理,可嵌入Webpack、Gulp等构建流程,适合需要定制化优化的项目。

const Fontmin = require('fontmin')
new Fontmin()
  .src('./src/fonts/xxx.ttf')
  .use(Fontmin.glyph({ text: '项目实际用到的文字', hinting: false }))
  .use(Fontmin.ttf2woff2())
  .dest('./dist/fonts')
  .run()

vite-plugin-fontmin,Vue3+Vite项目专属

# 安装插件
npm install vite-plugin-fontmin -D
# 或者yarn/pnpm
pnpm add vite-plugin-fontmin -D

配置文件,这里写的比较全,包含字体优化、资源打包、开发环境优化等等,可根据项目字体自行修改。

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// 引入字体子集化插件
import fontminPlugin from 'vite-plugin-fontmin'

export default defineConfig({
  // 路径别名
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  },
  // 插件配置
  plugins: [
    vue(),
    // 字体子集化核心配置
    fontminPlugin({
      // 配置多个字体(单字体直接写单个对象即可)
      fonts: [
        {
          // 源字体文件路径(放入项目src/fonts目录下)
          fontSrc: './src/fonts/SourceHanSansCN-Regular.ttf',
          // 子集化后字体的输出目录
          fontDest: './src/assets/fonts/subset/',
          // 自动扫描项目文件,提取所有用到的字符(无需手动书写)
          inputPath: ['./src/**/*.{vue,ts,tsx,js,jsx,css,scss}'],
          // 额外预留字符(动态内容、用户输入、接口返回文字,提前预留)
          input: '0123456789qwertyuiopasdfghjklzxcvbnm,。!?;:“”‘’',
          // 仅输出WOFF2格式
          formats: ['woff2'],
          // 开启unicode-range按需加载(核心)
          unicodeRange: true,
          // 字体渲染规则,避免阻塞
          fontDisplay: 'swap',
          // 关闭字体提示,进一步压缩体积
          hinting: false
        },
        // 多字体配置示例(标题字体,按需添加)
        {
          fontSrc: './src/fonts/TitleFont-Bold.ttf',
          fontDest: './src/assets/fonts/subset/title/',
          inputPath: ['./src/components/Title/**/*.vue', './src/views/**/*.vue'],
          formats: ['woff2'],
          unicodeRange: true,
          fontDisplay: 'swap'
        }
      ],
      // 开发环境仅执行一次子集化,避免热更新卡顿
      runOnceInDev: true,
      // 生产环境压缩字体
      compress: true
    })
  ],
  // 生产构建配置
  build: {
    assetsDir: 'static/assets',
    // 字体资源单独打包,方便缓存
    rollupOptions: {
      output: {
        assetFileNames: (assetInfo) => {
          if (assetInfo.name && assetInfo.name.endsWith('.woff2')) {
            return 'static/assets/fonts/[name]-[hash][extname]'
          }
          return 'static/assets/[name]-[hash][extname]'
        }
      }
    },
    // 关闭生产环境sourcemap,提升打包速度
    sourcemap: false,
    // 代码压缩
    minify: 'terser'
  },
  // 开发服务器配置
  server: {
    port: 3000,
    open: true
  }
})

完成上述配置以后,插件会自动生成对应的@font-face规则,无需手动引入字体CSS,直接在项目样式里使用即可。

/* src/assets/css/global.css */
body {
  font-family: 'SourceHanSansCN', sans-serif;
}
.title {
  font-family: 'TitleFont', sans-serif;
  font-weight: 700;
}

总结

其实字体优化一直是老大难问题,不上字体包效果出不来,上了字体包加载速度上不来。

当然,我们仍然建议,非必要不要上字体包。

还有一个"邪修"方案,可以手动创建字体包子集,也就是说这个字体包只包含要用字,其他的删掉。(参考iconFont的字体库"下载子集")。

如果非要上,那就是所有字体转WOFF2,拆分字体包,添加font-display: swap

另外再增加字体子集化和按需加载的部分,让首屏加载快起来。

昨天以前首页

别再瞎写 Cesium 可视化!热力图 + 四色图源码全公开,项目直接复用!

作者 李剑一
2026年3月24日 09:26

之前 Cesium 中一直围绕着园区进行开发,现在增加地图的部分,后面还会增加公共组件、统计图等等操作。

智慧园区、区域管控等三维GIS场景中,热力图四色图是两大最常用的可视化展示方案。

image.png

热力图直观展示密度、热度、强度分布,四色图清晰区分行政区域、功能分区、风险等级,两者搭配能让三维可视化效果和实用性直接拉满!

热力图

使用 Cesium 纯原生实现,无第三方依赖。

采用多级渐变色的效果,径向渐变过渡显得更加自然。

image.png

同时支持显示/隐藏切换,内存自动释放。

完整代码

const createHeatMap = () => {
    if (heatMapRef.value) {
        cesiumViewer.value.scene.primitives.remove(heatMapRef.value);
        heatMapRef.value = null;
    }
    
    // 热力图数据点(经纬度+权重值)
    const cameraList = [
        { longitude: 117.105914, latitude: 36.437846, height: 15 },
        { longitude: 117.105842, latitude: 36.437532, height: 14 },
        // 此处可替换为你的业务数据...
    ];
    
    // 创建Billboard集合
    const billboardCollection = new Cesium.BillboardCollection({
        scene: cesiumViewer.value.scene
    });
    
    // 计算最大权重,用于归一化
    const maxHeight = Math.max(...cameraList.map(c => c.height));
    
    // 热力图8级渐变色配置
    const heatColors = {
        veryLow: new Cesium.Color(0.0, 0.0, 1.0, 0.2),    // 深蓝
        low: new Cesium.Color(0.0, 0.5, 1.0, 0.3),        // 蓝色
        mediumLow: new Cesium.Color(0.0, 1.0, 1.0, 0.4),  // 青色
        medium: new Cesium.Color(0.5, 1.0, 0.5, 0.5),     // 黄绿色
        mediumHigh: new Cesium.Color(1.0, 1.0, 0.0, 0.6), // 黄色
        high: new Cesium.Color(1.0, 0.7, 0.0, 0.7),       // 橙色
        veryHigh: new Cesium.Color(1.0, 0.3, 0.0, 0.8),   // 橙红
        extreme: new Cesium.Color(1.0, 0.0, 0.0, 0.9)     // 红色
    };
    
    // 遍历数据生成热力点
    cameraList.forEach(camera => {
        const normalizedHeight = camera.height / maxHeight;
        let color, radius, alpha;
        
        // 8级密度分级,自动匹配颜色、半径、透明度
        if (normalizedHeight < 0.125) {
            color = heatColors.veryLow; radius = 40; alpha = 0.25;
        } else if (normalizedHeight < 0.25) {
            const intensity = (normalizedHeight - 0.125) / 0.125;
            color = Cesium.Color.fromBytes(0, Math.round(128 * intensity), 255, Math.round(255 * 0.3));
            radius = 40 + intensity * 15; alpha = 0.3;
        } else if (normalizedHeight < 0.375) {
            const intensity = (normalizedHeight - 0.25) / 0.125;
            color = Cesium.Color.fromBytes(0, 128 + Math.round(127 * intensity), 255 - Math.round(127 * intensity), Math.round(255 * 0.35));
            radius = 55 + intensity * 15; alpha = 0.35;
        } else if (normalizedHeight < 0.5) {
            const intensity = (normalizedHeight - 0.375) / 0.125;
            color = Cesium.Color.fromBytes(Math.round(128 * intensity), 255, 128 - Math.round(128 * intensity), Math.round(255 * 0.4));
            radius = 70 + intensity * 20; alpha = 0.4;
        } else if (normalizedHeight < 0.625) {
            const intensity = (normalizedHeight - 0.5) / 0.125;
            color = Cesium.Color.fromBytes(128 + Math.round(127 * intensity), 255, 0, Math.round(255 * 0.5));
            radius = 90 + intensity * 25; alpha = 0.5;
        } else if (normalizedHeight < 0.75) {
            const intensity = (normalizedHeight - 0.625) / 0.125;
            color = Cesium.Color.fromBytes(255, 255 - Math.round(178 * intensity), 0, Math.round(255 * 0.6));
            radius = 115 + intensity * 30; alpha = 0.6;
        } else if (normalizedHeight < 0.875) {
            const intensity = (normalizedHeight - 0.75) / 0.125;
            color = Cesium.Color.fromBytes(255, 77 - Math.round(77 * intensity), 0, Math.round(255 * 0.7));
            radius = 145 + intensity * 35; alpha = 0.7;
        } else {
            const intensity = (normalizedHeight - 0.875) / 0.125;
            color = Cesium.Color.fromBytes(255, 0, 0, Math.round(255 * 0.8));
            radius = 180 + intensity * 40; alpha = 0.8;
        }
        
        // 多层圆形叠加,实现渐变光晕效果
        const numCircles = 3;
        for (let i = 0; i < numCircles; i++) {
            const circleRadius = radius * (0.7 + i * 0.15);
            const circleAlpha = alpha * (0.8 - i * 0.2);
            
            // Canvas绘制径向渐变圆形
            const canvas = document.createElement('canvas');
            canvas.width = 256; canvas.height = 256;
            const context = canvas.getContext('2d');
            const gradient = context.createRadialGradient(128,128,0,128,128,128);
            
            gradient.addColorStop(0, `rgba(${Math.round(color.red * 255)}, ${Math.round(color.green * 255)}, ${Math.round(color.blue * 255)}, ${circleAlpha})`);
            gradient.addColorStop(0.7, `rgba(${Math.round(color.red * 255)}, ${Math.round(color.green * 255)}, ${Math.round(color.blue * 255)}, ${circleAlpha * 0.5})`);
            gradient.addColorStop(1, `rgba(${Math.round(color.red * 255)}, ${Math.round(color.green * 255)}, ${Math.round(color.blue * 255)}, 0)`);
            
            context.fillStyle = gradient;
            context.beginPath();
            context.arc(128,128,128,0,Math.PI*2);
            context.fill();
            
            // 添加到场景
            billboardCollection.add({
                position: Cesium.Cartesian3.fromDegrees(camera.longitude, camera.latitude, camera.height),
                image: canvas,
                width: circleRadius * 2,
                height: circleRadius * 2,
                scaleByDistance: new Cesium.NearFarScalar(100,1.0,1000,0.5),
                translucencyByDistance: new Cesium.NearFarScalar(100,1.0,500,0.3),
            });
        }
    });
    
    cesiumViewer.value.scene.primitives.add(billboardCollection);
    heatMapRef.value = billboardCollection;
    cesiumViewer.value.scene.requestRender();
    console.log('✅ 热力图创建成功');
};

加载GeoJson四色图实现

使用 fetch 加载标准GeoJson行政区划/面数据,数据可以从这个地址下载: datav.aliyun.com/portal/scho…

image.png

这里采用的是四色循环渲染,如果想要多种颜色也是一样的。

完整代码

const createColorMap = () => {
    // 已存在则先移除
    if (colorMapRef.value) {
        cesiumViewer.value.dataSources.remove(colorMapRef.value);
        colorMapRef.value = null;
    }
    
    // 加载GeoJson区域数据
    fetch('/json/济南市.geojson')
        .then(res => res.json())
        .then(geojsonData => {
            // 四色图配色
            const colorMap = [
                new Cesium.Color(0.0, 0.0, 1.0, 0.7),   // 蓝
                new Cesium.Color(0.0, 1.0, 0.0, 0.7),   // 绿
                new Cesium.Color(1.0, 1.0, 0.0, 0.7),   // 黄
                new Cesium.Color(1.0, 0.0, 0.0, 0.7),   // 红
            ];
            
            const dataSource = new Cesium.GeoJsonDataSource();
            dataSource.load(geojsonData, {
                stroke: Cesium.Color.WHITE,
                fill: colorMap[0],
                strokeWidth: 2,
                clampToGround: true
            }).then(ds => {
                const entities = ds.entities.values;
                let colorIndex = 0;
                
                entities.forEach(entity => {
                    if (entity.polygon) {
                        // 四色循环赋值
                        entity.polygon.material = colorMap[colorIndex % 4];
                        entity.polygon.outline = true;
                        entity.polygon.outlineColor = Cesium.Color.WHITE;
                        entity.polygon.outlineWidth = 2;
                        
                        // 拉伸高度(立体效果)
                        entity.polygon.extrudedHeight = 50;
                        entity.polygon.height = 0;
                        
                        colorIndex++;
                    }
                });
                
                cesiumViewer.value.dataSources.add(ds);
                colorMapRef.value = ds;
                
                // 自动飞掠到视图
                cesiumViewRef.value.flyToLocation({
                    longitude: 117.12,
                    latitude: 36.67,
                    height: 400000,
                    duration: 3,
                    heading: 0,
                    pitch: -90,
                });
                
                console.log('✅ 四色图创建成功');
            });
        });
};

总结

热力图+四色图是Cesium三维GIS可视化中最常用、最实用的两大功能。

这里提供的代码均为原生实现、生产环境可用,无需引入任何第三方库,直接复制即可运行。

热力图效果需要大量的数据才能看出效果,如果是人造数据我建议通过AI生成查看效果。

四色图这里也可以增加颜色显示,看起来更丰富。

别再瞎写了!Cesium 模型 360° 环绕,4 套源码全公开,项目直接用

作者 李剑一
2026年3月23日 21:12

之前在地图上展示的园区模型增加了选中效果,但是对于展示性质的大屏而言,内容一直是静态展示的效果肯定不好。

image.png

所以模型自动环绕展示是绝对的核心亮点功能!无论是展厅大屏演示、项目汇报、还是产品展示,流畅的360°环绕浏览都能让你的三维场景瞬间提升质感。

简单整理了4种开箱即用的环绕方案,从极简入门到专业级平滑动画全覆盖,适配不同项目需求,复制代码直接运行!

flyToLocation + 定时器(极简)

利用之前封装 cesium-viewer 组件做的 flyToLocation 方法实现此功能。

之前封装的 flyToLocation 方法其实就是 cesium.camera.flyTo 方法。

这个方案代码最简单,无需复杂数学计算。支持分段式环绕,适合快速实现需求

可控制停留时间、飞行速度、环绕点数。

image.png

完整代码

// 环绕展示工厂模型
const displayFactoryModel = () => {
    if (!cesiumViewRef.value) return;
    
    const centerLon = 117.104273; // 模型中心点经度
    const centerLat = 36.437867; // 模型中心点纬度
    const radius = 150; // 环绕半径(米)
    const height = 80; // 相机高度
    const duration = 2; // 每个角度飞行时间(秒)
    
    let currentAngle = 0;
    const totalAngles = 8; // 环绕8个点
    const angleStep = 360 / totalAngles;
    
    const flyToNextPoint = () => {
        // 计算当前角度的位置
        const rad = currentAngle * Math.PI / 180;
        const offsetX = radius * Math.cos(rad);
        const offsetY = radius * Math.sin(rad);
        
        // 计算新的经纬度(简化计算,适用于小范围)
        const newLon = centerLon + (offsetX / 111320 / Math.cos(centerLat * Math.PI / 180));
        const newLat = centerLat + (offsetY / 111320);
        
        // 计算相机朝向(朝向中心点)
        const heading = (currentAngle + 180) % 360;
        
        cesiumViewRef.value.flyToLocation({
            longitude: newLon,
            latitude: newLat,
            height: height,
            duration: duration,
            heading: heading,
            pitch: -30, // 稍微俯视的角度
            onComplete: () => {
                currentAngle = (currentAngle + angleStep) % 360;
                setTimeout(flyToNextPoint, 500); // 短暂停留后继续
            }
        });
    };
    
    flyToNextPoint();
};

lookAt + 帧动画(推荐)

这个方案相较于上面的方案比较好的一点就是真正的360°无缝平滑环绕

这种方案无卡顿、无跳跃,动画效果最自然,并且支持无限循环环绕。生产环境推荐首选方案

完整代码

// 环绕展示工厂模型
const displayFactoryModel = () => {
    if (!cesiumViewer.value) return;
    
    const center = Cesium.Cartesian3.fromDegrees(117.104273, 36.437867, 0);
    const radius = 150; // 环绕半径
    const height = 80; // 相机高度
    const duration = 20; // 完整环绕一周的时间(秒)
    
    let startTime = null;
    let animationId = null;
    
    const animateOrbit = (timestamp) => {
        if (!startTime) startTime = timestamp;
        const elapsed = (timestamp - startTime) / 1000;
        
        // 计算当前角度(0到2π)
        const angle = (elapsed / duration) * 2 * Math.PI;
        
        // 计算相机位置(圆形轨道)
        const cameraX = center.x + radius * Math.cos(angle);
        const cameraY = center.y + radius * Math.sin(angle);
        const cameraZ = center.z + height;
        const cameraPosition = new Cesium.Cartesian3(cameraX, cameraY, cameraZ);
        
        // 设置相机位置和朝向(看向中心点)
        cesiumViewer.value.camera.lookAt(
            cameraPosition,
            center, // 看向中心点
            new Cesium.Cartesian3(0, 0, 1)  // up方向
        );
        
        // 继续动画
        animationId = requestAnimationFrame(animateOrbit);
    };
    
    // 开始动画
    animationId = requestAnimationFrame(animateOrbit);
    
    // 返回停止函数(可选)
    return () => {
        if (animationId) {
            cancelAnimationFrame(animationId);
        }
    };
};

flyTo + 曲线路径

基于样条曲线生成平滑路径。支持自定义轨迹点、飞行总时长。

这个方案可以实现复杂环绕、俯冲、拉升等动作,但是如果不是动态模型运动没太大必要用这个。

完整代码

// 环绕展示工厂模型
const displayFactoryModel = () => {
    if (!cesiumViewer.value) return;
    
    const center = Cesium.Cartesian3.fromDegrees(117.104273, 36.437867, 0);
    const radius = 150;
    const height = 80;
    const points = 12; // 路径点数
    const duration = 30; // 总飞行时间
    
    // 创建路径点
    const positions = [];
    for (let i = 0; i <= points; i++) {
        const angle = (i / points) * 2 * Math.PI;
        const x = center.x + radius * Math.cos(angle);
        const y = center.y + radius * Math.sin(angle);
        const z = center.z + height;
        positions.push(new Cesium.Cartesian3(x, y, z));
    }
    
    // 相机沿路径飞行
    cesiumViewer.value.camera.flyTo({
        destination: positions,
        orientation: {
            heading: Cesium.Math.toRadians(0),
            pitch: Cesium.Math.toRadians(-30),
            roll: 0.0
        },
        duration: duration,
        complete: () => {
            console.log('✅ 环绕展示完成');
        }
    });
};

camera.rotate 旋转(极简)

这种方案代码量最少,一行核心逻辑。直接使用原地旋转视角,不改变相机位置。

但是展示效果一般,只能原地自转,不能环绕。

完整代码

// 环绕展示工厂模型
const displayFactoryModel = () => {
    if (!cesiumViewer.value) return;
    
    // 先飞到模型上方
    cesiumViewRef.value.flyToLocation({
        longitude: 117.104273,
        latitude: 36.437867,
        height: 80,
        duration: 3,
        heading: 0,
        pitch: -30,
        onComplete: () => {
            // 开始旋转
            let angle = 0;
            const rotateInterval = setInterval(() => {
                angle = (angle + 1) % 360;
                cesiumViewer.value.camera.setView({
                    orientation: {
                        heading: Cesium.Math.toRadians(angle),
                        pitch: Cesium.Math.toRadians(-30),
                        roll: 0.0
                    }
                });
            }, 50); // 每50ms旋转1度
            
            // 10秒后停止
            setTimeout(() => {
                clearInterval(rotateInterval);
                console.log('✅ 旋转结束');
            }, 10000);
        }
    });
};

总结

模型环绕展示是Cesium展示模型非常好的一种方案,尤其是做会议室大屏使用。

展示修效果还是相当不错的,如果没有特殊要求,可以考虑使用 lookAt+帧动画,这是综合体验最优的方案。

强烈建议大家使用此方案,流畅不卡顿,用户体验直接拉满!

❌
❌