Performance concerns when converting GTK image to greyscale
By commenting out various parts of grayscaleFromRGB, we can deduce that what is taking the most time is getElems (and possibly the computations after return). Indeed, for a 2000x1000 picture, there are 2M pixels (x3 or 4 channels), and getElems is putting them in a list word by word. Lists are quite inefficient for storing large amounts of data like pictures (at least 4 or 5 extra words per element, and the GC comes with an extra x2 factor).

To reduce memory usage, we can write a custom loop to process the elements in the array as we read them, like below. To reduce that further, don't use lists to store large amounts of data. It still takes a while to read millions of elements from memory byte by byte; there may be a better way using a specialized data processing library but I don't know it.

grayscaleFromRGB :: Image -&gt; IO [Word8]
grayscaleFromRGB img = do
    imgType &lt;- img `get` imageStorageType
    if imgType == ImageEmpty
        then
            return (replicate 256 0)
        else do
            pixbuf &lt;- imageGetPixbuf img
            pixels &lt;- (pixbufGetPixels pixbuf :: IO (PixbufData Int Word8))
            nChannels &lt;- pixbufGetNChannels pixbuf
            (_, bound) &lt;- getBounds pixels
            loop pixels nChannels bound 0 []

    where
        -- the returned list will be reversed compared to the original code
        loop pixels n bound i acc | i + 2 &gt; bound = return acc
        loop pixels n bound i acc = do
          let get i = fromIntegral &lt;$&gt; (readArray (pixels :: PixbufData Int Word8) i) :: IO Double
          a &lt;- (0.3 *) &lt;$&gt; get i
          b &lt;- (0.59 *) &lt;$&gt; get (i + 1)
          c &lt;- (0.11 *) &lt;$&gt; get (i + 2)
          loop pixels n bound (i + n) $! (((:) $! round (a + b + c)) $! acc)