我做了好几年的 Web 自动化测试,Selenium 用得多了,最常被问到的一个问题就是:“脚本跑起来太慢了,一个用例动不动就几分钟,怎么办?”
确实,Selenium 自动化测试跑得慢是整个行业的老大难。但很多人没搞清楚一件事:慢的根源往往不是 Selenium 本身,而是你在用它的时候踩了一堆隐蔽的坑。比如不合理地等待页面加载、每次用例都重新启动浏览器、元素定位写得太烂……这些问题叠加起来,测试时间能被拉长 3 到 5 倍。这篇文章我就结合自己实际项目里的踩坑和优化经验,把 Selenium 自动化测试网页加载慢的核心原因、定位方法和解决方案一次讲透。不管你是刚入门的测试新人,还是被用例耗时折磨的资深测试,都能在这里找到可以参考的思路。
1. 先搞清楚:你的慢到底慢在哪?
1.1 慢的三种常见形态
我在接手团队测试框架的时候,第一件事不是急着改代码,而是先看用例耗时分布。因为“网页加载慢”是一个很模糊的症状,背后可能是完全不同的问题。根据我观察到的实际现象,大体可以分成三类:
第一类是单次访问页面特别慢。也就是浏览器打开一个 URL,要等好几秒甚至十几秒才能完成加载。这种情况常见于被测系统的首页图片资源多、接口响应慢,或者测试环境网络本身就不稳定。但这里有个容易被忽略的点:Selenium 的driver.get()默认会等待页面onload事件触发。如果页面上有什么第三方统计脚本一直在加载、或者某个接口挂起不返回,driver.get()就会一直等下去,直到超时。很多你以为的“页面加载慢”,其实是onload事件被阻塞了。
第二类是定位元素等待时间过长。这类问题更普遍。比如你写了time.sleep(5),不管页面 1 秒就加载完还是 5 秒加载完,它都固定等 5 秒。如果每个页面操作前都这么来一下,一个用例跑下来,光是死等的时间就能占到总耗时的一半以上。
第三类是整体跑批时间太长。单个用例不慢,但整个测试套件跑下来要一两个小时。这个往往是架构层面的问题,比如用例之间没有做浏览器复用、没有开并行执行,导致大量时间浪费在重复启动浏览器、加载空白页、初始化驱动这些环节上。
1.2 用简单方法给“慢”做初步定位
要判断到底属于哪一类,不需要上什么高级工具,加日志就行。我在项目里常用的做法是给关键步骤打点:
import time from selenium import webdriver start = time.time() driver = webdriver.Chrome() print(f"启动浏览器耗时: {time.time() - start:.2f} 秒") start = time.time() driver.get("http://your-test-site.com") print(f"页面加载耗时: {time.time() - start:.2f} 秒") start = time.time() element = driver.find_element(By.ID, "username") print(f"元素定位耗时: {time.time() - start:.2f} 秒")看到分段耗时后再下结论。我见过太多人一上来就改代码,结果改了半天发现瓶颈根本不是自己想的那块。另外有个经验可以分享:如果一次性打开 10 个页面都慢,那大概率是被测系统或网络的问题;如果只有个别页面慢,那可能是页面本身的资源加载问题;如果每次跑脚本都比手动操作慢很多,那问题就出在自动化代码的等待策略上。
2. 核心优化第一步:调整页面加载策略
2.1 pageLoadStrategy 的三种模式
Selenium 的 WebDriver 在加载页面时有一个pageLoadStrategy(页面加载策略),它直接决定了driver.get()要等到什么时候才算“加载完成”。默认值是normal,也就是要等页面触发onload事件才返回。但很多页面根本不关心onload,它的核心内容早在 DOM Ready 的时候就可以操作了。
我就遇到过这样一个案例:被测系统首页加载了一堆数据可视化图表,图表渲染完成要好几秒,但登录框这种核心元素在 DOM 加载后立即就能操作。结果自动化脚本里driver.get()硬是等了图表全部渲染完才往下走,白白浪费了 5 秒多。
页面加载策略提供了三种选项:
| 策略 | 等待内容 | 适用场景 |
|---|---|---|
normal | 等待页面 onload 事件触发 | 默认值,稳定但慢 |
eager | 等待 DOM 完全加载,不等待样式、图片和子框架 | 页面核心元素在 DOM 中就绪的场景 |
none | 不等待任何加载事件,立即返回 | 需要完全手动控制等待的场景 |
2.2 实际配置示例
修改策略很简单,在启动浏览器的时候设置即可:
from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.page_load_strategy = "eager" # 使用 eager 策略 driver = webdriver.Chrome(options=options) driver.get("http://your-test-site.com")我把这个改到项目里之后,首页加载时间直接从平均 8.2 秒降到了 3.1 秒,提升非常明显。但这里要提醒一句:换成eager之后,后续的元素定位代码必须配上合理的显式等待,否则很容易因为元素还没渲染出来而报找不到元素的错。你不能既想省时间,又不想写等待,鱼和熊掌不能兼得。
对于页面元素极其简单的场景(比如纯接口返回数据渲染的页面),也可以用none策略,然后再根据业务逻辑自己决定什么时候开始找元素。不过这个策略对脚本编写水平要求更高,新手不建议直接用。
3. 等待策略的重构:把“死等”变成“等得准”
3.1 隐式等待与显式等待的误区
等待策略是自动化测试慢的“重灾区”,也是优化空间最大的地方。我几乎每次帮别人排查用例慢的问题,最后都能找到几处time.sleep()或者全局性的隐式等待设置。
先说隐式等待(implicitly_wait)。它的工作机制是:设置一个全局超时时间,之后每次通过driver.find_element查找元素时,如果元素没找到,WebDriver 会在超时时间内持续轮询 DOM。这看起来挺美,但坑在于:隐式等待是全局性的,你没法针对某个特定元素设置不同的等待时间。另外,隐式等待与显式等待在某些情况下会互相干扰,比如元素一直存在但处于不可点击状态时,隐式等待不会帮你判断可点击性,而显式等待可以做到。
再说显式等待(WebDriverWait)。它允许你针对特定元素、特定条件设置超时时间和轮询频率,这是目前推荐的写法。但很多人的用法是写time.sleep(3)代替显式等待,这本质上就是把不确定性变成了固定等待,页面快的时候浪费时间,页面慢的时候又不够用。
3.2 显式等待的正确姿势
正确的做法是只等“必要条件”,也就是等你要操作的那个元素出现在可交互状态。举个登录用例的例子:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def wait_element(driver, by, value, timeout=10): return WebDriverWait(driver, timeout).until( EC.presence_of_element_located((by, value)) ) # 等登录按钮可点击 login_btn = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, "login-btn")) ) login_btn.click()这里的关键是:把等待条件写对。如果你只是要元素存在,用presence_of_element_located;如果要操作它,建议用element_to_be_clickable或者visibility_of_element_located。这两个条件的语义完全不同,用错了就会出现“元素找到了但点击没反应”之类的玄学问题。
3.3 自定义等待函数的实战价值
比显式等待更进一步的做法是封装自定义等待函数。比如有些场景要等一个异步请求返回后页面才出现某些内容,这时候标准条件就不太好使了。我在框架里通常会封装一个通用的轮询函数:
def wait_until(driver, condition_func, timeout=15, interval=0.5, description=""): """轮询等待自定义条件成立,条件不满足时持续重试直到超时""" start_time = time.time() last_error = None while time.time() - start_time < timeout: try: if condition_func(driver): return True except Exception as e: last_error = e time.sleep(interval) raise TimeoutError( f"等待超时({timeout}秒): {description}" + (f",最后错误: {last_error}" if last_error else "") )这样封装之后,测试代码里就完全不需要time.sleep()了。需要等什么条件就传什么条件,等到就继续,等不到就报超时错误,报错信息还自带描述,排查问题也方便。我实际改造过的项目里,等请求返回的场景从固定 sleep 8 秒降到平均 1.5 秒,单看一个操作可能感觉不多,但整个回归套件几十个用例加起来,省的时间就很可观了。
4. 浏览器与网络环境层面的加速手段
4.1 浏览器启动参数优化
等待策略优化之后,如果还是觉得慢,可以看看浏览器本身能不能做减法。Chrome 启动时默认会加载一堆扩展、同步设置、自动填充等,这些对自动化测试没任何用处,纯属拖时间。我在项目里常用的无头加禁用参数配置是这样的:
options = webdriver.ChromeOptions() # 无头模式运行,节省资源 options.add_argument("--headless=new") # 禁用 GPU 硬件加速 options.add_argument("--disable-gpu") # 禁用浏览器扩展 options.add_argument("--disable-extensions") # 禁用自动填充 options.add_argument("--disable-autofill") # 禁用通知弹窗 options.add_argument("--disable-notifications") # 禁止加载图片(对测试无影响的场景可启用) prefs = {"profile.managed_default_content_settings.images": 2} options.add_experimental_option("prefs", prefs)这里重点说一下图片拦截。如果你的被测系统对图片不敏感(比如后端管理后台、接口测试页面),把图片加载关掉能带来很大的性能提升。有一次我在一个查询功能测试里关掉了图片加载,页面加载时间直接少了 40%,因为那个页面左侧挂了张很大的企业宣传图,每轮操作都要重新拉一次。当然,如果你测的是电商前台、设计稿这种图片敏感的场景,这个配置就不能开。
4.2 用性能日志定位资源瓶颈
有时候页面等得久是因为有超大资源的加载。这时候可以打开 Chrome 的性能日志来看看到底是谁慢:
caps = { "goog:loggingPrefs": {"performance": "ALL"}, "goog:perfLoggingPrefs": { "enableNetwork": True, "enablePage": True, }, } options.set_capabilities("goog:loggingPrefs", caps) logs = driver.get_log("performance") for entry in logs: message = json.loads(entry["message"])["message"] method = message["method"] if method == "Network.responseReceived": params = message["params"] response = params.get("response", {}) mime_type = response.get("mimeType", "") url = params.get("url", "") # 过滤大图片、大脚本、大样式文件 if mime_type.startswith("image/") or mime_type in ("application/javascript", "text/css"): print(f"资源加载: {response.get('status')} {url}")通过日志里每个网络请求的响应时间,你可以很直观地看到是哪个脚本、哪张图片拖慢了速度,然后决定是前端配合优化资源,还是在测试环境里用 hosts 拦截。我一般会从这条日志里挑出响应时间超过 1 秒的资源,反馈给开发团队一起排查,效率和自动化测试本身都能受益。
4.3 本地静态资源替换加速
还有一个偏门但很有效的技巧:把被测页面里稳定的大静态资源(比如 logo、字体文件)在本地起一个代理或者直接改 hosts 映射到内网快速资源。这种操作只对测试环境有效,需要开发或运维配合,但效果立竿见影。
做这些优化的时候,一个原则要守住:优化的前提是不影响测试结果的有效性。如果你的用例确实要验证图片是否展示、样式是否正常,那就不能禁用图片加载。优化不是一刀切,要根据具体场景灵活调整。
5. 从架构层面解决:并行执行与浏览器复用
5.1 pytest-xdist 实现用例级并行
单条脚本优化到位之后,下一步就是解决整体跑批时间的问题。我最推荐的方式是使用pytest-xdist插件做用例级并行,因为它对现有代码的侵入性最小,配置起来非常快。
pip install pytest-xdist然后在运行测试时指定并发数:
pytest -n 4这会启动 4 个 worker 进程,每条用例会被自动分配到空闲的 worker 上执行。如果你的用例彼此没有状态依赖(这是自动化测试的基本要求),并行起来几乎没有成本。
但并行执行有个容易踩的坑:数据库测试数据冲突。比如多个 worker 同时执行“创建订单”的用例,可能都会选择同一条测试数据,然后互相修改,导致断言失败。我的经验是,要么给每条用例分配独立的数据范围(比如用随机前缀),要么在用例前置里动态创建干净的数据,而不是依赖一个共享的固定测试数据集。另外,并行的数量不是越多越好,我习惯根据测试机 CPU 核数来定,一般 4 到 8 个并发在普通办公机上表现最好,开太多反而会因为资源竞争变慢。
5.2 Selenium Grid 实现分布式执行
如果单机资源有限,或者需要跨浏览器兼容性测试,可以考虑 Selenium Grid。它让测试用例在不同机器上分布式地启动浏览器,典型架构是 1 个 Hub 管理多个 Node 节点。我这里给一个简单的 docker-compose 快速启动方案:
version: "3.8" services: selenium-hub: image: selenium/hub:4.15.0 container_name: selenium-hub ports: - "4444:4444" environment: - GRID_MAX_SESSION=10 chrome-node: image: selenium/node-chrome:4.15.0 shm_size: 2gb depends_on: - selenium-hub environment: - SE_EVENT_BUS_HOST=selenium-hub - SE_EVENT_BUS_PUBLISH_PORT=4442 - SE_EVENT_BUS_SUBSCRIBE_PORT=4443启动之后,脚本里指向 Hub 地址即可:
from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() driver = webdriver.Remote( command_executor="http://localhost:4444/wd/hub", options=options )用 Selenium Grid 之后,用例可以同时跑在多个浏览器实例上,整个回归从 40 分钟压缩到 8 分钟以内很常见。但要注意,Grid 对测试机的内存要求比较高,每个 Chrome 实例大概要占 300 到 500MB 内存,起 8 个并发之前先确认清楚。
5.3 浏览器会话复用的思路
还有一种优化思路是复用浏览器会话,也就是一个测试用例跑完不关浏览器,下一个用例继续用。这个在 Selenium 里原生不太好做,但是可以通过debuggerAddress连接已启动的浏览器来实现:
# 先手动启动带调试端口的 Chrome # chrome --remote-debugging-port=9222 from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.debugger_address = "localhost:9222" driver = webdriver.Chrome(options=options)这样连上的就是一个“真人在使用”的浏览器上下文,不仅省掉了重复启动浏览器的时间,还能直接操作你手动登入后的登录态,处理那些和登录态耦合的用例非常方便。这个思路在调试阶段很有用,CI 环境里不建议这么搞,因为不稳定。
6. 常见问题与排查技巧实录
6.1 常见问题速查表
我在优化 Selenium 测试速度的过程中,积累了一批典型的“病因”和对应的“药方”,整理成表分享给大家:
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
driver.get()长时间不返回 | 页面 onload 被阻塞 | 改用eager策略;检查第三方脚本 |
| 元素时有时无,偶发失败 | 等待策略不明确 | 统一使用显式等待,不写sleep |
| 每次启动浏览器都要好几秒 | 驱动和浏览器版本不匹配 | 升级到 Selenium 4,用 Selenium Manager 自动管理驱动 |
| 用例单独跑很快,全部跑很慢 | 存在用例间的状态污染 | 用 pytest-xdist 并行,并保证用例独立 |
| 页面元素找得到但点击没反应 | 等待条件用错 | 改用element_to_be_clickable |
| 读取表格数据慢 | 用了逐行逐格定位 | 改用一次读取整个表格文本再解析 |
6.2 排查路线图
如果遇到“自动化测试页面加载慢”的问题,我建议按这个步骤排查,不要跳过顺序:
- 先看单次
driver.get()耗时,确认是不是所有页面都慢。如果都慢,先排除网络和测试环境问题,再考虑加载策略的调整。 - 统计整个用例的耗时分布,把启动浏览器、导航、定位元素、断言几个阶段分别打印时间,定位最耗时的阶段。
- 检查测试代码里有没有
time.sleep()。这是一个信号,凡是出现基本都可以改成显式等待。 - 打开性能日志看网络请求,定位是不是某个大资源拖慢了页面。
- 从架构上考虑,是不是可以并行、是否可以复用浏览器。
按这个流程走下来,90% 以上的“慢”都能找到明确原因并给出针对性的改进方案。
6.3 独家避坑技巧:耐心与取舍
最后分享一个我很早就踩过、直到现在还经常有人踩的坑:不要为了快而牺牲用例稳定性。有些同学把page_load_strategy改成none、又把所有等待全部删掉,表面上用例跑得飞快,但一旦系统响应稍慢就批量失败,然后回来改等待,又变得很慢。这是一个恶性循环。我个人的体会是:稳定的优先级永远高于速度。优化的正确路径是先保证稳定,再逐步压缩不必要的耗时,而不是反过来。每个优化项加进去之后,至少观察一轮完整的回归测试,确认失败率没有上升,再继续下一步优化。
另外一个实用的小技巧是:日志级别调成 DEBUG,在一轮失败的用例里翻一下执行日志的时间戳,很多问题一眼就能看出来。比如你发现两次定位操作之间隔了 5 秒,那就意味着等待有问题;如果日志显示页面加载返回后和下一次操作没有间隔,说明状态已经异常了。排查慢的问题和排查功能 bug 是一样的,需要数据和证据,不能靠猜。
还有一个我一直在用的做法:给用例加上分级。冒烟测试用例坚决要求最快,回归测试里相对稳定但耗时的用例单独标记出来,跑批时用不同的策略执行。比如冒烟测试用eager加显式等待,全套回归才用默认的normal策略做兜底。这样既保证了冒烟速度,又降低了回归的风险。
Selenium 自动化测试的提速没有一招鲜的办法,它更像是一个持续优化的过程。每次你解决一个慢的问题,你对这套工具和被测系统的理解都会加深一层。希望上面这些经验能帮你少走一些弯路,把更多时间花在真正值得测的事情上。