Perhaps GDAL helps here.
You can create a basemap using the GDAL WMS driver and clip it with gdalwarp to a virtual format file (VRT).
To increase loading performance, enable local caching inside the WMS description XML (https://gdal.org/drivers/raster/wms.html).
In the following example (basemap of Vienna) I'm using a small trick to add the WKT description of the image boundary via SQL (gdalwarp cutline without using a shapefile).
With the GDAL config option "GDALWARP_DENSIFY_CUTLINE" set to "NO" we keep the VRT size small (Very large VRT file generated by gdal after cutline).
And here we go:
1.) Copy the following line into the OSGeo4W shell and press ENTER. If you get no VRT file, you have to change the path of the qgis.db and/or the path of the Cache directory and the resulting VRT file
gdalwarp -r near -of VRT --config GDALWARP_DENSIFY_CUTLINE NO -cutline "C:\OSGeo4W\apps\qgis\resources\qgis.db" -csql "select ST_GeomFromText('Polygon((1801649 6135190, 1803604 6132890, 1805904 6132085, 1804984 6131165, 1806249 6129210, 1806249 6127830, 1810964 6128635, 1814874 6127715, 1816139 6126680, 1817634 6129440, 1821888 6128520, 1825798 6127485, 1826718 6126680, 1829708 6126910, 1829593 6129900, 1831548 6130130, 1835228 6132890, 1838217 6133120, 1841437 6130820, 1845577 6129670, 1845232 6133465, 1843162 6134040, 1842012 6136225, 1841207 6136570, 1841552 6138755, 1841092 6140135, 1842127 6143354, 1842702 6146804, 1841207 6147034, 1842012 6150829, 1839597 6150599, 1837412 6152669, 1838562 6154739, 1834883 6155429, 1834653 6152554, 1830168 6155314, 1829708 6159683, 1827983 6160488, 1825568 6159683, 1824648 6160258, 1823383 6159339, 1823843 6157269, 1823038 6156349, 1822578 6154049, 1820623 6154164, 1820048 6155429, 1817634 6153014, 1813494 6151864, 1813839 6151174, 1812459 6149794, 1810504 6149104, 1809469 6146689, 1808204 6146689, 1807859 6148184, 1804179 6150599, 1803144 6148644, 1802454 6146574, 1802914 6145309, 1802339 6144159, 1803029 6143239, 1804294 6141744, 1803489 6138525, 1801649 6135190))')" -dstalpha "<GDAL_WMS><Service name='TMS'><ServerUrl>https://maps1.wien.gv.at/basemap/geolandbasemap/normal/google3857/${z}/${y}/${x}.png</ServerUrl></Service><DataWindow><UpperLeftX>-20037508.34</UpperLeftX><UpperLeftY>20037508.34</UpperLeftY><LowerRightX>20037508.34</LowerRightX><LowerRightY>-20037508.34</LowerRightY><TileLevel>15</TileLevel><TileCountX>1</TileCountX><TileCountY>1</TileCountY><YOrigin>top</YOrigin></DataWindow><Projection>EPSG:3857</Projection><BlockSizeX>256</BlockSizeX><BlockSizeY>256</BlockSizeY><BandsCount>3</BandsCount><UserAgent>QGIS</UserAgent><Cache><Path>c:/temp/gdalwmscache</Path><Extension>.png</Extension></Cache></GDAL_WMS>" vienna.vrt
2.) Drag & Drop the VRT file into QGIS map canvas. Unfortuantely, you won't see the basemap yet, even if you try to zoom to the extents. Looks like QGIS is not able to calculate the extents of the virtual raster. But you can insert the original XYZ layer with the URL "https://maps.wien.gv.at/basemap/geolandbasemap/normal/google3857/{z}/{y}/{x}.png" (Max. Zoom Level 18) and zoom to its extents. Then zoom to Vienna (Wien).
You will recognize, that the clipped image has a different zoom level than the XYZ layer. I guess this happens because QGIS processes the two datasources differently. But you can freeze/lock the maximum zoom level inside the WMS XML description to a lower value - i.e. 12 (search for "<TileLevel>"). With a lower max. level value you even prevent to get unreadable text labels in printouts.

Best of all, you can replace the GDAL datasource of your clipped basemap with the content of the VRT to store it directly in your QGIS project! Use the invaluable plugin "changeDataSource" for the replacement.

