The default Export is creating an intermediate Image object with a higher ImageResolution. If you watch the link traffic in LinkSnooper, you can see the ExportPacket request go to the FE and the Image[] being returned. On my system, that Image[] is ImageResolution->144, but yours might be different because it's going to use whatever the native image resolution for that window is (I'm running on a Windows system with 150% scaling).
I'm not 100% for certain what the Export code does after that, but if you take your code, tweak the Rasterize to sample at 144dpi, and then do a straight 50% reduction using ImageResize, it comes out looking very much like what Export generates.
ImageResize[
Rasterize[img, ImageSize -> is, ImageResolution -> 144], is]
But...no need to do the ImageResize yourself. If you simply send the image to Export and add an ImageSize option for Export, it works pretty well.