|
| 1 | +--- |
| 2 | +title: 'Hybrid approach to performance' |
| 3 | +heading: 'Hybrid performance with k6 browser' |
| 4 | +head_title: 'Hybrid performance with k6 browser' |
| 5 | +excerpt: 'An example on how to implement a hybrid approach to performance with k6 browser' |
| 6 | +weight: 01 |
| 7 | +--- |
| 8 | + |
| 9 | +# Hybrid performance with k6 browser |
| 10 | + |
| 11 | +An alternative approach to [browser-based load testing](https://grafana.com/docs/k6/<K6_VERSION>/testing-guides/load-testing-websites/#browser-based-load-testing) that is much less resource-intensive is combining a low amount of virtual users for a browser test with a high amount of virtual users for a protocol-level test. |
| 12 | + |
| 13 | +Hybrid performance can be achieved in multiple ways, often using different tools. To simplify the developer experience, k6 browser can be easily combined with core k6 features, so you can easily write hybrid tests in a single script. |
| 14 | + |
| 15 | +## Browser and HTTP test |
| 16 | + |
| 17 | +The code below shows an example of combining a browser and HTTP test in a single script. While the backend is exposed to the typical load, the frontend is also checked for any unexpected issues. Thresholds are defined to check both HTTP and browser metrics against pre-defined SLOs. |
| 18 | + |
| 19 | +{{< code >}} |
| 20 | + |
| 21 | +```javascript |
| 22 | +import http from "k6/http"; |
| 23 | +import { check } from "k6"; |
| 24 | +import { browser } from "k6/experimental/browser"; |
| 25 | + |
| 26 | +const BASE_URL = __ENV.BASE_URL; |
| 27 | + |
| 28 | +export const options = { |
| 29 | + scenarios: { |
| 30 | + load: { |
| 31 | + exec: 'getPizza', |
| 32 | + executor: 'ramping-vus', |
| 33 | + stages: [ |
| 34 | + { duration: '5s', target: 5 }, |
| 35 | + { duration: '10s', target: 5 }, |
| 36 | + { duration: '5s', target: 0 }, |
| 37 | + ], |
| 38 | + startTime: '10s', |
| 39 | + }, |
| 40 | + browser: { |
| 41 | + exec: 'checkFrontend', |
| 42 | + executor: 'constant-vus', |
| 43 | + vus: 1, |
| 44 | + duration: '30s', |
| 45 | + options: { |
| 46 | + browser: { |
| 47 | + type: 'chromium', |
| 48 | + }, |
| 49 | + }, |
| 50 | + } |
| 51 | + }, |
| 52 | + thresholds: { |
| 53 | + http_req_failed: ['rate<0.01'], |
| 54 | + http_req_duration: ['p(95)<500', 'p(99)<1000'], |
| 55 | + browser_web_vital_fcp: ["p(95) < 1000"], |
| 56 | + browser_web_vital_lcp: ["p(95) < 2000"], |
| 57 | + }, |
| 58 | +}; |
| 59 | + |
| 60 | +export function getPizza() { |
| 61 | + let restrictions = { |
| 62 | + maxCaloriesPerSlice: 500, |
| 63 | + mustBeVegetarian: false, |
| 64 | + excludedIngredients: ['pepperoni'], |
| 65 | + excludedTools: ['knife'], |
| 66 | + maxNumberOfToppings: 6, |
| 67 | + minNumberOfToppings: 2 |
| 68 | + } |
| 69 | + |
| 70 | + let res = http.post(`${BASE_URL}/api/pizza`, JSON.stringify(restrictions), { |
| 71 | + headers: { |
| 72 | + 'Content-Type': 'application/json', |
| 73 | + 'X-User-ID': customers[Math.floor(Math.random() * customers.length)], |
| 74 | + }, |
| 75 | + }); |
| 76 | + |
| 77 | + check(res, { |
| 78 | + 'status is 200': (res) => res.status === 200 |
| 79 | + }); |
| 80 | +} |
| 81 | + |
| 82 | +export async function checkFrontend() { |
| 83 | + const page = browser.newPage(); |
| 84 | + |
| 85 | + try { |
| 86 | + await page.goto(BASE_URL) |
| 87 | + check(page, { |
| 88 | + 'header': page.locator('h1').textContent() == 'Looking to break out of your pizza routine?', |
| 89 | + }); |
| 90 | + |
| 91 | + await page.locator('//button[. = "Pizza, Please!"]').click(); |
| 92 | + page.waitForTimeout(500); |
| 93 | + page.screenshot({ path: `screenshots/${__ITER}.png` }); |
| 94 | + |
| 95 | + check(page, { |
| 96 | + 'recommendation': page.locator('div#recommendations').textContent() != '', |
| 97 | + }); |
| 98 | + } finally { |
| 99 | + page.close(); |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +``` |
| 104 | + |
| 105 | +{{< /code >}} |
| 106 | + |
| 107 | +## Browser and failure injection test |
| 108 | + |
| 109 | +A browser test can also be run with a failure injection test via the [xk6-disruptor](https://github.com/grafana/xk6-disruptor) extension. This approach lets you find issues in your front end if any services it depends on are suddenly injected with failures, such as delays or server errors. |
| 110 | + |
| 111 | +The code below shows an example of introducing faults to a Kubernetes service. While this happens, the `browser` scenario is also executed, which checks the frontend application for unexpected errors that might not have been handled properly. |
| 112 | + |
| 113 | +To find out more information about injecting faults to your service, check out our [get started guide with xk6-disruptor](https://grafana.com/docs/k6/<K6_VERSION>/javascript-api/xk6-disruptor/get-started/). |
| 114 | + |
| 115 | +{{< code >}} |
| 116 | + |
| 117 | +```javascript |
| 118 | +import http from "k6/http"; |
| 119 | +import { check } from "k6"; |
| 120 | +import { browser } from "k6/experimental/browser"; |
| 121 | + |
| 122 | +const BASE_URL = __ENV.BASE_URL; |
| 123 | + |
| 124 | +export const options = { |
| 125 | + scenarios: { |
| 126 | + disrupt: { |
| 127 | + executor: "shared-iterations", |
| 128 | + iterations: 1, |
| 129 | + vus: 1, |
| 130 | + exec: "disrupt", |
| 131 | + }, |
| 132 | + browser: { |
| 133 | + executor: "constant-vus", |
| 134 | + vus: 1, |
| 135 | + duration: "10s", |
| 136 | + startTime: "10s", |
| 137 | + exec: "browser", |
| 138 | + options: { |
| 139 | + browser: { |
| 140 | + type: "chromium", |
| 141 | + }, |
| 142 | + }, |
| 143 | + }, |
| 144 | + }, |
| 145 | + thresholds: { |
| 146 | + browser_web_vital_fcp: ["p(95) < 1000"], |
| 147 | + browser_web_vital_lcp: ["p(95) < 2000"], |
| 148 | + }, |
| 149 | +}; |
| 150 | + |
| 151 | +// Add faults to the service by introducing a delay of 1s and 503 errors to 10% of the requests. |
| 152 | +const fault = { |
| 153 | + averageDelay: "1000ms", |
| 154 | + errorRate: 0.1, |
| 155 | + errorCode: 503, |
| 156 | +} |
| 157 | + |
| 158 | +export function disrupt() { |
| 159 | + const disruptor = new ServiceDisruptor("pizza-info", "pizza-ns"); |
| 160 | + const targets = disruptor.targets(); |
| 161 | + if (targets.length == 0) { |
| 162 | + throw new Error("expected list to have one target"); |
| 163 | + } |
| 164 | + |
| 165 | + disruptor.injectHTTPFaults(fault, "20s"); |
| 166 | +} |
| 167 | + |
| 168 | +export async function checkFrontend() { |
| 169 | + const page = browser.newPage(); |
| 170 | + |
| 171 | + try { |
| 172 | + await page.goto(BASE_URL) |
| 173 | + check(page, { |
| 174 | + 'header': page.locator('h1').textContent() == 'Looking to break out of your pizza routine?', |
| 175 | + }); |
| 176 | + |
| 177 | + await page.locator('//button[. = "Pizza, Please!"]').click(); |
| 178 | + page.waitForTimeout(500); |
| 179 | + page.screenshot({ path: `screenshots/${__ITER}.png` }); |
| 180 | + |
| 181 | + check(page, { |
| 182 | + 'recommendation': page.locator('div#recommendations').textContent() != '', |
| 183 | + }); |
| 184 | + } finally { |
| 185 | + page.close(); |
| 186 | + } |
| 187 | +} |
| 188 | + |
| 189 | +``` |
| 190 | + |
| 191 | +{{< /code >}} |
| 192 | + |
| 193 | +## Recommended practices |
| 194 | + |
| 195 | +- **Do start small**. Start with a low amount of browser-based virtual users. A good starting point is to have 10% virtual users or less to monitor the user experience for your end-users, while around 90% of traffic should be emulated from the protocol level. |
| 196 | +- **Combine browser test with different load testing types**. To fully understand the impact of different traffic patterns on your end-user experience, experiment with running your browser test with different [load testing types](https://grafana.com/docs/k6/<K6_VERSION>/testing-guides/test-types/). |
| 197 | +- **Cover high-risk user journeys as a start**. Consider identifying the high-risk user journeys first so you can start to monitor the web performance metrics for these user journeys while your backend applications are being exposed to high traffic or service faults. |
0 commit comments